綴Photo の勉強ノートに戻る
コードレビュー学習2026.05.11

「ベストプラクティス全部入れ」をあえてやらない — DRF throttle のキャッシュを Redis にせず据え置いた理由

DRFDjangorate limit判断軸

Django REST framework の throttle 周りで指摘された「キャッシュ未設定」を、Redis を入れる前に判断軸で整理した話です。

この記事でわかること

  • DRF の ScopedRateThrottle がキャッシュをどう使っているか
  • 「CACHES 未設定」のままだとプロセス内メモリで動くこと
  • マルチプロセス / マルチインスタンスで何が起きるか
  • Redis を入れるか、「外側(Cloudflare)に寄せる方針」で当面 LocMem のままにするか、判断する軸

起きていた指摘

このプロジェクトの設定は:

python
# 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 まで」を カウンタで管理します。

回数は各 worker のプロセス内メモリ(LocMemCache)。DB のテーブルには無い。案内係(ロードバランサ)は関所。回数は持たない。B に回すworker Aプロセス内メモリ LocMemCacheログイン試行 9 / 10あと 1 回で止め再起動で消える。DB には書かないworker Bプロセス内メモリ LocMemCacheログイン試行 0 / 10まだ 10 回試せる再起動で消える。DB には書かないB に回すと上限 10/min が実効 20/min になる(worker 数倍)。サーバ DB に回数の列は無い。残る場所は各プロセスのメモリだけである。

再起動でカウンタが消えるのも、攻撃者が「再起動を待てば回避できる」状態。

直し方の候補

案 内容 コスト
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 だけ。

回数のメモ帳は Django のプロセス内メモリ。DB のテーブルにも Cloudflare にも無い。今将来(公開フェーズ)エッジ(L4-L7)CloudflareDNS only・プロキシ無効WAF / rate limit は未設定回数は無いCloudflare WAF / rate limitDDoS / 高頻度アクセス既知の悪パターンエッジで広く受けるアプリDjango プロセス内メモリLocMemCacheログイン 10/min など今の唯一の防衛線Django プロセス内メモリ同一アカウント 10回/分までアプリ固有の粒度エッジの取りこぼしの二次防衛

採用した判断

このプロジェクトの規模では:

  • ログイン系の外側防御は将来 Cloudflare 側(WAF / rate limit)に寄せる方針(公開フェーズで設定予定)
  • Django throttle は プロセス内メモリのまま残す = worker scale up しないなら影響軽微
  • 「Redis 不採用 / Cloudflare 側で守る」をコードに明示

実装は production settings に CACHES を明示するだけ:

python
# 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 を入れるか

明確にトリガー条件を書いておく:

  1. Render でインスタンス / worker scale up する必要が出た時 = カウンタ分散が実害になるタイミング
  2. 本気の自動化攻撃でアプリ層 throttle を効かせたい時 = Cloudflare 側に WAF / rate limit を入れた後、そこで取りこぼした攻撃の二次防衛が要るタイミング
  3. キャッシュを throttle 以外(セッション / view cache 等)でも使い始めた時 = ついでに Redis 化

これが来るまでは LocMemCache でいい、と決めておくことで「今やるべきか / 後でいいか」がブレない。

学びポイント

  • DRF throttle のカウンタは CACHES に依存します。LocMemCache だと worker / プロセス間で分散します
  • 守る場所は エッジ(Cloudflare)と アプリ(Django)で分担できます。両方を理解した上で選びます
  • 「Redis を入れる」決定は固定費発生 = 入れない決定を意識して残す価値があります
  • 暗黙のデフォルトより、明示 + 判断コメントのほうが半年後に強いです

おわりに

「ベストプラクティス」を全部入れる方が正しい場面もあるが、プロジェクトの規模・予算・優先順位を見て「やらない」を選ぶことも判断のうち。

判断したらコードに痕跡を残すこと。次に開く時に「なぜこうなってたんだっけ?」を考え直さなくて済みます。


この記事は2026年5月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。