Django REST framework の throttle 周りで指摘された「キャッシュ未設定」を、Redis を入れる前に判断軸で整理した話です。
この記事でわかること
- DRF の
ScopedRateThrottleがキャッシュをどう使っているか - 「CACHES 未設定」のままだとプロセス内メモリで動くこと
- マルチプロセス / マルチインスタンスで何が起きるか
- Redis を入れるか、「外側(Cloudflare)に寄せる方針」で当面 LocMem のままにするか、判断する軸
起きていた指摘
このプロジェクトの設定は:
# REST_FRAMEWORK
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.ScopedRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"shared_page_verify": "10/min",
"auth_login": "10/min",
"auth_signup": "5/min",
"auth_token_refresh": "60/min",
},ログイン / サインアップ / トークン更新 / 共有ページ password 確認に rate limit が当たっている。挙動としては正しい。
でも CACHES が未設定だった。
これだと Django がデフォルトの LocMemCache(プロセスローカルメモリ) にフォールバックする = throttle のカウンタがプロセスごとに別になります。
レビュー指摘:
Render の instance / worker が増えると試行回数制限が分散する。 プロセス再起動でカウンタが消える。
何が問題か
DRF の throttle は「IP やユーザごとに N/min まで」を カウンタで管理します。
再起動でカウンタが消えるのも、攻撃者が「再起動を待てば回避できる」状態。
直し方の候補
| 案 | 内容 | コスト |
|---|---|---|
| A Django CACHES に Redis を入れる | 共通ストレージで全 worker 間でカウンタ共有 | Render Redis $7/月〜 + 接続管理 |
| B Cloudflare WAF / rate limit に任せる | プロダクト前面の L4-L7 で制限 | 無料プランで一定数まで |
| C LocMemCache のまま明示する | プロセス内のままで運用 | コードコメントだけ |
このプロジェクトの状況
判断材料を並べると:
- 現状の構成: Vercel (frontend) + Render (backend)。ドメイン / DNS は Cloudflare(Registrar / DNS)。前段プロキシとしての WAF / rate limit はまだ有効化していない
- 想定トラフィック: 公開ローンチ前、身内 + 限定共有
- Render プラン: Standard 以下なら instance = 1 / worker 数も最小
- 撤退判定期間: 〜2026-10、月5万までは固定費を増やしたくない
- 本気の攻撃面: ログイン試行回数等のアプリ固有粒度は Django 側、L4-L7 トラフィック制限は Cloudflare 側
どこで何を守るかの分担
「Cloudflare で守る vs Django で守る」は対立じゃなくて役割分担だ。ただし正直に言うと、今の綴Photo は Cloudflare 側の WAF / rate limit をまだ設定しておらず、エッジ層は空。現時点で実際に効いているのは Django throttle だけ。
採用した判断
このプロジェクトの規模では:
- ログイン系の外側防御は将来 Cloudflare 側(WAF / rate limit)に寄せる方針(公開フェーズで設定予定)
- Django throttle は プロセス内メモリのまま残す = worker scale up しないなら影響軽微
- 「Redis 不採用 / Cloudflare 側で守る」をコードに明示
実装は production settings に CACHES を明示するだけ:
# production.py
# 2026-05-11 判断: 当面 LocMemCache(プロセス内メモリ)で運用する。
# - Render の Standard プラン以下は instance = 1 / worker 数も最小 = プロセス内メモリで実害なし
# - レート制限は現状 Django の ScopedRateThrottle が唯一の層。Cloudflare は DNS only
# (プロキシ無効)で前段の WAF / rate limit としては効いていないため、アプリ層 throttle
# (ログイン試行回数等)がそのまま主防衛。将来 Cloudflare proxy を有効化すれば前段でも受けられる
# - 撤退判定期間(〜2026-10)で固定費を増やしたくないため Redis 採用は保留
# Redis 採用トリガー:
# - Render で worker / instance scale up する必要が出た時
# - 本気の自動化攻撃でアプリ層 throttle を効かせたい時
# - 採用候補は Render Redis($7/月〜)。settings.CACHES['default'] を
# django_redis.cache.RedisCache に切り替える1点修正で動く想定。
CACHES = {
"default": {
# ← さっきの「レジ係が手元のメモ帳に書く」方式。これを"意図して"選んでいる
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "tsuzuphoto-throttle", # このメモ帳に付けた名札
},
}ポイント:
- 暗黙のデフォルトに頼らない: CACHES 未設定だと Django のデフォルトに依存します。意図して LocMemCache を選んでいることをコードで表現します
- 判断ログを残す: 「なぜ Redis にしなかったか」をコメントで残す = 半年後の自分(or 他人)が読んで意図がわかります
- 切り替えのトリガーを明示: いつ Redis に切り替えるべきかを書いておく = 「そろそろ?」と思った時の判断材料
- コメントに「現状の砦は Django throttle だけ」と正直に書く: 「外側(エッジ)に寄せる」は将来の方針であって完成形ではない、という現在地をコメントに残します
「未設定」から「明示」への意義
機能的には CACHES = {} と CACHES = {"default": {"BACKEND": "...LocMemCache", ...}} は同じ。
でもコードとしての意味が違います:
- 未設定 = 「ここを考えていない / デフォルトに依存している」
- 明示 = 「LocMemCache を選んでいる / 理由はコメント参照」
レビュー時 / 引き継ぎ時に「意図が見える」のが価値。
いつ Redis を入れるか
明確にトリガー条件を書いておく:
- Render でインスタンス / worker scale up する必要が出た時 = カウンタ分散が実害になるタイミング
- 本気の自動化攻撃でアプリ層 throttle を効かせたい時 = Cloudflare 側に WAF / rate limit を入れた後、そこで取りこぼした攻撃の二次防衛が要るタイミング
- キャッシュを throttle 以外(セッション / view cache 等)でも使い始めた時 = ついでに Redis 化
これが来るまでは LocMemCache でいい、と決めておくことで「今やるべきか / 後でいいか」がブレない。
学びポイント
- DRF throttle のカウンタは CACHES に依存します。LocMemCache だと worker / プロセス間で分散します
- 守る場所は エッジ(Cloudflare)と アプリ(Django)で分担できます。両方を理解した上で選びます
- 「Redis を入れる」決定は固定費発生 = 入れない決定を意識して残す価値があります
- 暗黙のデフォルトより、明示 + 判断コメントのほうが半年後に強いです
おわりに
「ベストプラクティス」を全部入れる方が正しい場面もあるが、プロジェクトの規模・予算・優先順位を見て「やらない」を選ぶことも判断のうち。
判断したらコードに痕跡を残すこと。次に開く時に「なぜこうなってたんだっけ?」を考え直さなくて済みます。
この記事は2026年5月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。