DRFスロットリング(リクエスト制限)完全攻略:なぜ必要か、どう設定・適用・カスタムするか

nginxリバースプロキシでlimit_reqを使って「入口を切る」ことは確かに効果的です。しかしビュー/アクションごとに異なるポリシー(例:ログインは1分5回、アップロードは1日20回、閲覧APIはユーザーあたり1000回)を課したい場合、サーバー/インフラの設定だけに頼るのは難しいです。DRFのスロットリングはアプリケーションレベルで「エンドポイント特性」に合わせた制限を作れる点で「必ず知っておくべき基本技」です。


スロットリングが必要な理由



DRFスロットリングは「権限(permission)」のようにリクエストを許可するかどうかを決定しますが、違いは永続的(権限)vs 一時的(リクエスト頻度制限) です。DRFのドキュメントでも、スロットリングは「クライアントがAPIに送るリクエストのレートを制御する仕組み」と説明されています。

現実的な必要性は大まかにこうまとめられます。

  • 濫用/攻撃緩和:ブルートフォース(ログイン)、スパムリクエスト、クローリング、単純DoSなど
  • コスト/リソース保護:アップロード、外部API呼び出し、重いクエリ、生成型AI呼び出しなど「高価」なエンドポイント
  • 公平性(fair use):特定ユーザー/キーがリソースを独占しないように
  • ポリシーのコード化:"このAPIは1分N回"などのルールをインフラではなくコードで管理

そして重要なポイント:スロットリングはグローバル(global)でも、特定ビュー/アクション単位でも設定できます。(ここでnginxだけでは不十分になることが多い)


DRFスロットリングが動作する仕組み(コア概念のみ)

1) レート文字列(Rate string)

DRFは通常"100/day""60/min"などの形式で制限を設定します。

2) 「誰を」制限するか(クライアント識別)

  • UserRateThrottle:認証ユーザーならuser id、未認証ならIPで制限
  • AnonRateThrottle未認証(anonymous)リクエストのみIPで制限
  • ScopedRateThrottle:"このビューはuploadsスコープ"などscope単位のポリシーを適用

IP識別はX-Forwarded-ForまたはREMOTE_ADDRを使用し、プロキシ後にある場合はNUM_PROXIES設定が重要です。

3) 状態保存はCacheを使用

DRF基本スロットリング実装はDjango cache backendにカウントを保存します。単一プロセス/単一サーバーならデフォルトのLocMemCacheで動作しますが、マルチワーカー・マルチレプリカ環境ではRedisなど共有キャッシュが実質必須です。


グローバル(Global)設定:最速スタート



settings.py:

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)を返します。


ビューに適用する方法:エンドポイント別に異なる設定

1) クラスベースビュー(APIView)で指定

グローバルポリシーと別に特定ビューだけスロットリングを異なる値に設定できます。

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)で指定

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)

"リスト取得はゆるく、特定POSTアクションは厳しく"などのポリシーに有効です。

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で「ビュー特性別」ポリシー作成(強推)

Scopedスロットリングは「このビューはuploads、別ビューはlogin」など意味ある名前(scope)でポリシーを分離でき、運用がスッキリします。

settings.py:

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.ScopedRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "login": "5/min",
        "uploads": "20/day",
        "search": "60/min",
    },
}

ビューでscopeだけ宣言:

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作成:"キーをどう取るか"がコア

組み込みスロットリングでも十分ですが、実務ではこういった要件が頻出します。

  • "ログインはIP + usernameの組み合わせで制限したい"
  • "API Key別に制限したい"
  • "特定ヘッダー/テナント/組織単位で制限したい"
  • "キャッシュをdefaultではなく別キャッシュ(例:redis cluster)で使いたい"

1) 一般的な方法:SimpleRateThrottle継承

get_cache_key()だけを適切に定義すれば「何を基準に制限するか」を自由に変えられます。

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を登録:

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "path.to.LoginBurstThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "login": "5/min",
    },
}

2) キャッシュを別に使いたい場合(cache属性)

DRFドキュメントにあるように、カスタムスロットリングでcacheを変更できます。

from django.core.cache import caches
from rest_framework.throttling import AnonRateThrottle

class CustomCacheAnonThrottle(AnonRateThrottle):
    cache = caches["alternate"]

デプロイ前に必ず知っておきたい!

1) プロキシ環境でIPが同じように取られる問題

IP識別はX-Forwarded-For/REMOTE_ADDRに基づきます。プロキシ後ならNUM_PROXIESを正確に設定しないと「全ユーザーが1人」と扱われる事故が起きます。

2) LocMemCacheはマルチワーカー/マルチサーバーに弱い

キャッシュがプロセスローカルならワーカーごとにカウントが分離します。運用ではRedisなど共有キャッシュが安全です(スロットリングが「正しく」動作するため)。

3) 同期競合(race condition)

DRF組み込み実装は高同時性で数リクエストが余分に通過できる競合条件があると文書で明記しています。"正確にN回で切る"必要がある決済/クーポンなどの場合は、Redis INCR + EXPIREなど原子カウントでカスタム実装を検討してください。

4) クライアントフレンドリー:429とRetry-After

DRFは制限時にデフォルトで429を返します。

またスロットリングのwait()を実装すればRetry-Afterヘッダーを含められます。


まとめ:nginxとDRFスロットリング、両方取り入れよう

ロボットクラブの入場制限と429表示のネオンサイン

  • nginx:大量トラフィック/攻撃を最前線でカットするシールド
  • DRFスロットリング:エンドポイントの意味とコストを知るアプリケーションレベルで精密なポリシー適用

特に「ビューごとに特性に合わせた制限」はDRFスロットリングが最も手軽で、サーバ環境が変わってもコードで維持できるため強力です。