# DRFスロットリング(リクエスト制限)完全攻略:なぜ必要か、どう設定・適用・カスタムするか nginxリバースプロキシで`limit_req`を使って「入口を切る」ことは確かに効果的です。しかし**ビュー/アクションごとに異なるポリシー**(例:ログインは1分5回、アップロードは1日20回、閲覧APIはユーザーあたり1000回)を課したい場合、サーバー/インフラの設定だけに頼るのは難しいです。DRFのスロットリングは**アプリケーションレベルで「エンドポイント特性」に合わせた制限**を作れる点で「必ず知っておくべき基本技」です。 --- ## スロットリングが必要な理由 {#sec-c97ae217f027} DRFスロットリングは「権限(permission)」のように**リクエストを許可するかどうか**を決定しますが、違いは**永続的(権限)vs 一時的(リクエスト頻度制限)** です。DRFのドキュメントでも、スロットリングは「クライアントがAPIに送るリクエストのレートを制御する仕組み」と説明されています。 現実的な必要性は大まかにこうまとめられます。 * **濫用/攻撃緩和**:ブルートフォース(ログイン)、スパムリクエスト、クローリング、単純DoSなど * **コスト/リソース保護**:アップロード、外部API呼び出し、重いクエリ、生成型AI呼び出しなど「高価」なエンドポイント * **公平性(fair use)**:特定ユーザー/キーがリソースを独占しないように * **ポリシーのコード化**:"このAPIは1分N回"などのルールをインフラではなくコードで管理 そして重要なポイント:スロットリングは**グローバル(global)**でも、**特定ビュー/アクション単位**でも設定できます。(ここでnginxだけでは不十分になることが多い) --- ## DRFスロットリングが動作する仕組み(コア概念のみ) {#sec-a5c59acab781} ### 1) レート文字列(Rate string) {#sec-a2424ad14298} DRFは通常`"100/day"`、`"60/min"`などの形式で制限を設定します。 ### 2) 「誰を」制限するか(クライアント識別) {#sec-9cf8b4c551fc} * `UserRateThrottle`:認証ユーザーなら**user id**、未認証なら**IP**で制限 * `AnonRateThrottle`:**未認証(anonymous)**リクエストのみIPで制限 * `ScopedRateThrottle`:"このビューはuploadsスコープ"など**scope単位**のポリシーを適用 IP識別は`X-Forwarded-For`または`REMOTE_ADDR`を使用し、プロキシ後にある場合は`NUM_PROXIES`設定が重要です。 ### 3) 状態保存はCacheを使用 {#sec-7b9c8a453729} DRF基本スロットリング実装は**Django cache backend**にカウントを保存します。単一プロセス/単一サーバーならデフォルトの`LocMemCache`で動作しますが、マルチワーカー・マルチレプリカ環境ではRedisなど共有キャッシュが実質必須です。 --- ## グローバル(Global)設定:最速スタート {#sec-c94dcae484bb} `settings.py`: ```python REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "anon": "100/day", "user": "1000/day", }, } ``` これで「基本ポリシー」が全APIに適用されます。 リクエストが制限されるとDRFはデフォルトで**HTTP 429(Too Many Requests)**を返します。 --- ## ビューに適用する方法:エンドポイント別に異なる設定 {#sec-491e225aa51d} ### 1) クラスベースビュー(APIView)で指定 {#sec-f32b57a3aba1} グローバルポリシーと別に特定ビューだけスロットリングを異なる値に設定できます。 ```python from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.throttling import UserRateThrottle class ExpensiveView(APIView): throttle_classes = [UserRateThrottle] def get(self, request): return Response({"ok": True}) ``` ドキュメントでも`throttle_classes`でビュー単位の適用を案内しています。 ### 2) 関数ベースビュー(@api_view)で指定 {#sec-7a28800ffd4e} ```python from rest_framework.decorators import api_view, throttle_classes from rest_framework.throttling import UserRateThrottle from rest_framework.response import Response @api_view(["GET"]) @throttle_classes([UserRateThrottle]) def ping(request): return Response({"pong": True}) ``` ### 3) ViewSetの特定アクションにのみ適用(@action) {#sec-f2285f7a57f9} "リスト取得はゆるく、特定POSTアクションは厳しく"などのポリシーに有効です。 ```python from rest_framework.decorators import action from rest_framework.throttling import UserRateThrottle from rest_framework.viewsets import ViewSet from rest_framework.response import Response class ItemViewSet(ViewSet): @action(detail=True, methods=["post"], throttle_classes=[UserRateThrottle]) def purchase(self, request, pk=None): return Response({"purchased": pk}) ``` アクションに設定したスロットリングはViewSetレベル設定より優先されます。 --- ## ScopedRateThrottleで「ビュー特性別」ポリシー作成(強推) {#sec-69d1c4813e63} Scopedスロットリングは「このビューはuploads、別ビューはlogin」など**意味ある名前(scope)**でポリシーを分離でき、運用がスッキリします。 `settings.py`: ```python REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.ScopedRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "login": "5/min", "uploads": "20/day", "search": "60/min", }, } ``` ビューでscopeだけ宣言: ```python from rest_framework.views import APIView from rest_framework.response import Response class LoginView(APIView): throttle_scope = "login" def post(self, request): return Response({"ok": True}) ``` DRFは`ScopedRateThrottle`使用時に、**scope + ユーザーID(未認証ならIP)** でユニークキーを作り、カウントします。` --- ## カスタムThrottle作成:"キーをどう取るか"がコア {#sec-892da9d3fb29} 組み込みスロットリングでも十分ですが、実務ではこういった要件が頻出します。 * "ログインはIP + usernameの組み合わせで制限したい" * "API Key別に制限したい" * "特定ヘッダー/テナント/組織単位で制限したい" * "キャッシュをdefaultではなく別キャッシュ(例:redis cluster)で使いたい" ### 1) 一般的な方法:SimpleRateThrottle継承 {#sec-759164135e01} `get_cache_key()`だけを適切に定義すれば「何を基準に制限するか」を自由に変えられます。 ```python from rest_framework.throttling import SimpleRateThrottle class LoginBurstThrottle(SimpleRateThrottle): scope = "login" def get_cache_key(self, request, view): username = (request.data.get("username") or "").lower().strip() ident = self.get_ident(request) # IPベース識別(プロキシ設定影響) if not username: return None # usernameが無い場合はスロットリング適用しない(必要に応じて変更) return f"throttle_login:{ident}:{username}" ``` そしてsettingsにscope rateを登録: ```python REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "path.to.LoginBurstThrottle", ], "DEFAULT_THROTTLE_RATES": { "login": "5/min", }, } ``` ### 2) キャッシュを別に使いたい場合(cache属性) {#sec-99ae4b21e2b9} DRFドキュメントにあるように、カスタムスロットリングで`cache`を変更できます。 ```python from django.core.cache import caches from rest_framework.throttling import AnonRateThrottle class CustomCacheAnonThrottle(AnonRateThrottle): cache = caches["alternate"] ``` --- ## デプロイ前に必ず知っておきたい! {#sec-68bf2bd36ec0} ### 1) プロキシ環境でIPが同じように取られる問題 {#sec-55e331e40267} IP識別は`X-Forwarded-For`/`REMOTE_ADDR`に基づきます。プロキシ後なら`NUM_PROXIES`を正確に設定しないと「全ユーザーが1人」と扱われる事故が起きます。 ### 2) LocMemCacheはマルチワーカー/マルチサーバーに弱い {#sec-238e19948ccf} キャッシュがプロセスローカルならワーカーごとにカウントが分離します。運用ではRedisなど共有キャッシュが安全です(スロットリングが「正しく」動作するため)。 ### 3) 同期競合(race condition) {#sec-d87294622fb5} DRF組み込み実装は高同時性で**数リクエストが余分に通過**できる競合条件があると文書で明記しています。"正確にN回で切る"必要がある決済/クーポンなどの場合は、Redis INCR + EXPIREなど原子カウントでカスタム実装を検討してください。 ### 4) クライアントフレンドリー:429とRetry-After {#sec-f405894aac21} DRFは制限時にデフォルトで429を返します。 またスロットリングの`wait()`を実装すれば`Retry-After`ヘッダーを含められます。 --- ## まとめ:nginxとDRFスロットリング、両方取り入れよう {#sec-b08cdd5cec16} ![ロボットクラブの入場制限と429表示のネオンサイン](/media/editor_temp/6/c96bb1d6-7cde-48a0-98aa-07396570391d.png) * nginx:大量トラフィック/攻撃を**最前線で**カットするシールド * DRFスロットリング:**エンドポイントの意味とコスト**を知るアプリケーションレベルで精密なポリシー適用 特に「ビューごとに特性に合わせた制限」はDRFスロットリングが最も手軽で、サーバ環境が変わってもコードで維持できるため強力です。