skip to content

In Django Ninja, how do AnonRateThrottle, AuthRateThrottle and UserRateThrottle differ in which requests they count and how they key them?

level: middleimportance: should knowfreq 30%

answer

  1. instances with rate strings
  2. runs after authentication
  3. IP, auth value or user id
  4. anonymous fallback for two of them

basics

~20 s

AnonRateThrottle counts only requests without request.auth, keyed by client IP; AuthRateThrottle keys by a hash of str(request.auth); UserRateThrottle keys by the Django request.user's primary key. The last two fall back to IP for anonymous callers.

solid answer

~40 s

Django Ninja throttles are **instances** passed to `throttle=` on the API, a router or an operation, usually with a rate string such as `AuthRateThrottle("600/m")`. They run after authentication, so they can see `request.auth`. `AnonRateThrottle` (scope `anon`) ignores requests where `request.auth` is set and keys the rest by client IP. `AuthRateThrottle` (scope `auth`) keys by a SHA-256 of `str(request.auth)`, falling back to IP. `UserRateThrottle` (scope `user`) keys by `request.user.pk` - the Django session user - also falling back to IP. So for an `APIKeyHeader` partner, `AuthRateThrottle` counts per partner while `UserRateThrottle` would count per IP, because `request.user` stays anonymous. A throttled request gets 429 `{"detail": "Too many requests."}` with a `Retry-After` header.

code

python · 17 lines
python
from ninja import NinjaAPI, Router
from ninja.throttling import AnonRateThrottle, AuthRateThrottle

api = NinjaAPI(throttle=AnonRateThrottle("30/m"))  # public endpoints, per IP

partners = Router(
    auth=PartnerKey(),
    throttle=AuthRateThrottle("600/m"),  # replaces the API-level throttle, per partner
)


@partners.post("/orders/bulk", throttle=AuthRateThrottle("10/m"))  # overrides the router
def bulk_orders(request):
    return {"accepted": True}


api.add_router("/partners/", partners)

go deeper

for a junior

Recall that throttles are instances with a rate string on throttle=, and that exceeding one returns 429.

for a middle

Explain which identity each class keys on - IP, str(request.auth) or request.user.pk - and when they fall back to IP.

for a senior

Pick the class that matches the authentication in use, make request.auth's string form unique, and know that failed authentication is never throttled.

for a principal

Decide which limits belong in Ninja and which in front of the application, since in-app throttles only see requests that reached Django.

## Declaring throttles In **Django Ninja**, a **throttle** limits how often a caller may hit an operation. Throttles come from `ninja.throttling` and are passed as **instances**, not classes, to `throttle=`: - `NinjaAPI(throttle=[AnonRateThrottle("10/s"), AuthRateThrottle("100/s")])` - every operation; - `Router(throttle=...)` or `api.add_router(..., throttle=...)` - one router; - `@api.post("/orders", throttle=AuthRateThrottle("60/m"))` - one operation. As with `auth=`, the most specific level **replaces** the others: an operation's `throttle=` overrules the router's and the API's, and a router's overrules the API's. ## Rate strings and defaults A rate is `requests/period`. The period is a number with an optional unit - `s`/`sec`, `m`/`min`, `h`/`hour`, `d`/`day` - and a bare number means seconds, so `100/5m`, `100/300s` and `100/300` all mean 100 requests per 5 minutes. When no rate is passed, the class looks up its **scope** in the `NINJA_DEFAULT_THROTTLE_RATES` setting, whose built-in values are: | scope | class | default rate | |---|---|---| | `anon` | `AnonRateThrottle` | `1000/day` | | `auth` | `AuthRateThrottle` | `10000/day` | | `user` | `UserRateThrottle` | `10000/day` | ## What each class counts and keys on | class | counts | key for an identified caller | key otherwise | |---|---|---|---| | `AnonRateThrottle` | only requests with no `request.auth` | - (skipped) | client IP | | `AuthRateThrottle` | every request | SHA-256 of `str(request.auth)` | client IP | | `UserRateThrottle` | every request | `request.user.pk` | client IP | The difference between the last two is **which identity** they read: - `request.auth` is whatever the Ninja authenticator returned - a `Partner` from an `APIKeyHeader` subclass, a `User` from `django_auth`. - `request.user` is set by Django's `AuthenticationMiddleware` from the session. An API-key caller has no session, so `request.user` is anonymous and `UserRateThrottle` counts the partner by IP. Because `AuthRateThrottle` hashes `str(request.auth)`, the object an authenticator returns needs a `__str__` that is unique and stable per caller. ## When a request is throttled Throttles run in the operation's checks **after** authentication and before parameter validation: 1. Authentication runs; a failure returns 401 immediately, before any throttle. 2. Every throttle on the operation is asked `allow_request()`; each records the request in its history if it allows it. 3. If any throttle refused, Ninja raises `Throttled`, an `HttpError` subclass rendered as **429** `{"detail": "Too many requests."}`. 4. The largest wait reported by the refusing throttles, rounded up to whole seconds, goes into the **`Retry-After`** header. The history is a list of timestamps stored through Django's cache framework under a key built from the scope and the identity, so it lives wherever the project's default cache lives. ## Custom and disabled throttles The built-ins are small classes, and extending them is routine: - **Custom rule** - subclass `BaseThrottle` and implement `allow_request(request)` returning `True` or `False`, optionally with `wait()` returning seconds for `Retry-After`; or subclass a built-in and call `super().allow_request()` only for the requests you want counted, such as non-GET methods. - **Custom scope** - set `scope = "partner_bulk"` on a subclass and either pass a rate to the constructor or add the scope to `NINJA_DEFAULT_THROTTLE_RATES`; a scope with neither raises `ImproperlyConfigured` when the instance is created. - **No throttle for one operation** - because the most specific level replaces the others, `throttle=[]` on an operation leaves it with no throttles even when the router or API has some. Ninja's throttles are modelled on DRF's, and a DRF-style custom throttle often ports over; the visible difference is that Ninja takes instances rather than classes. ## Choosing for a partner API - **Partners authenticated by API key**: `AuthRateThrottle("600/m")`, with `Partner.__str__` returning something like `partner:<pk>`. - **Public, unauthenticated endpoints**: `AnonRateThrottle`, keyed by IP. - **Browser users on `django_auth`**: `UserRateThrottle` or `AuthRateThrottle` both identify the Django user. - **Mixed surfaces**: `[AnonRateThrottle("30/m"), AuthRateThrottle("600/m")]` gives anonymous callers a tight limit, which the auth throttle's IP fallback also counts, and identified callers a looser one. A custom rule - such as not throttling reads - is a subclass overriding `allow_request()`; the counting algorithm itself is a sliding log of timestamps inherited from `SimpleRateThrottle`.

  • How do you change the default rate for every AnonRateThrottle() created without an argument?
    Set `NINJA_DEFAULT_THROTTLE_RATES` in Django settings, for example `{"anon": "100/hour", "auth": "5000/day", "user": "5000/day"}`. The throttle looks its scope up in that mapping when it is constructed without a rate; a scope missing from it raises `ImproperlyConfigured`.
  • Are unauthenticated requests to an auth-protected Django Ninja operation counted by its throttles?
    No. Authentication runs first, and a failure returns 401 before any throttle is asked. Throttles on that operation only ever count requests that already passed authentication, so they do not slow down someone guessing keys.

saying these in an interview costs you the question

  • UserRateThrottle counts API-key partners by the partner returned in request.auth.
  • AnonRateThrottle also counts authenticated requests, just keyed by IP.
  • Throttles are passed as classes, like DRF's DEFAULT_THROTTLE_CLASSES.
  • Router and API throttles are combined, so both limits apply.
  • Throttles run before authentication, so they cannot see request.auth.