Anonymous, user, and scoped throttling
coreintermediateA throttle counts requests against a key and refuses once the count exceeds a rate. DRF ships three classes that differ only in how the key is derived. `AnonRateThrottle` throttles unauthenticated callers by IP address. `UserRateThrottle` throttles by user id when signed in, falling back to the IP when not. `ScopedRateThrottle` reads a `throttle_scope` attribute off the view and applies the rate configured for that scope, which is how one endpoint gets a tighter limit than the rest of the API. Rates are strings like `"100/hour"` or `"5/min"`, and when a caller exceeds one, DRF returns 429 with a `Retry-After` header saying how long to wait.
Think of it as
A throttle is a counter keyed by "who", and the choice of key is the whole design. Keying on IP is the only option for anonymous traffic, and it is a blunt one: a corporate NAT, a university, or a mobile carrier puts thousands of unrelated people behind one address, so an IP limit generous enough not to break them is generous enough to be useless against a determined single attacker. Keying on user id is precise but only exists after authentication, which is exactly when you least need protection — the expensive-to-abuse endpoints are usually login and password-reset, where there is no user yet. So the two classes are not alternatives; they cover different halves of the traffic and belong in the list together. Scoped throttling then handles the fact that "requests per hour" is a bad unit for a whole API: reading a cached list and sending a verification email do not deserve the same budget. The scope makes the limit a property of the endpoint rather than of the project, which is what lets the global rate stay generous while the few genuinely expensive operations stay tight.
What we're doing: A generous global budget, with the two endpoints worth abusing held to a much tighter one.
- 3–4
- Both classes, not one. `AnonRateThrottle` covers pre-login traffic by IP; `UserRateThrottle` gives each account its own budget once identity exists.
- 9–10
- Scope rates live in the same dict as the defaults. The scope name is arbitrary — it just has to match the view's `throttle_scope`.
- 17
- Setting `throttle_classes` on the view *replaces* the default list rather than adding to it, so this endpoint is now governed by the scope alone — deliberate here, since 5/min is stricter than either default.
- 24
- Three password resets an hour per IP. Low enough to stop enumeration, high enough that a real person who mistypes their email twice is not locked out.
Why this works: A single global rate has to be loose enough for the busiest legitimate client, which makes it useless on the endpoints that send email or verify credentials. Scoping moves the limit to where the cost is, so the general API stays fast and the expensive operations stay protected.
Setting `throttle_classes` on a view and losing the defaults you meant to keep
Wrong
Better
What you see: Nothing looks wrong — the scope limit works, and the endpoint returns 429 at the expected rate. The loss is invisible: the account-wide budget that was supposed to cap total usage across all endpoints no longer counts this one.
Why: DRF resolves `throttle_classes` as an override, not an addition, exactly like `permission_classes` and `authentication_classes`. Every class in the list runs and any one of them can reject, so listing the defaults alongside the scoped class is what keeps both limits in force. This is easy to miss because the symptom is an absence of enforcement rather than an error.
- A request arrives
- AnonRateThrottle — key = client IP
- Unauthenticated only — signed-in requests skip it entirely
- One key per address — a NAT puts thousands of people in one bucket
- The only option before login — which is where the abuse actually is
- UserRateThrottle — key = user.pk, or IP when anonymous
- Precise for signed-in callers — one account cannot hide behind a shared address
- The general per-account budget — generous — it is a backstop, not the real defence
- Falls back to IP — so it also covers anonymous traffic, coarsely
- ScopedRateThrottle — key = scope + user.pk or IP
- throttle_scope = "login" — the limit becomes a property of the endpoint
- Tight where cost is high — password reset, email send, export, search
- No scope, no throttle — a view without the attribute is untouched by this class
The three shipped throttle classes
Together
Remember: A throttle is a counter keyed on "who". `AnonRateThrottle` keys on IP and covers only unauthenticated traffic; `UserRateThrottle` keys on the user id (IP when anonymous) — list both, since they cover different halves. `ScopedRateThrottle` moves the limit onto the endpoint via `throttle_scope`, which is how login and password reset get 5/min while the API keeps a generous default. Setting `throttle_classes` on a view replaces the defaults rather than adding to them, and behind a proxy you must set `NUM_PROXIES` or the IP key is meaningless.
See also: throttle backends and multiple instances · abuse prevention and quotas · authentication order anonymous users and failures

