skip to content

In Django REST Framework, how do you switch on AnonRateThrottle and UserRateThrottle, and what does a throttled client receive back?

level: juniorimportance: must knowfreq 55%

answer

  1. off until you list classes
  2. two settings: classes and rates
  3. 'number/period', first letter counts
  4. the Throttled exception and a header

basics

~20 s

DRF throttles nothing by default: list the classes in DEFAULT_THROTTLE_CLASSES and give 'anon' and 'user' rates like '100/day' in DEFAULT_THROTTLE_RATES. A throttled request gets 429 with a Retry-After header and a 'Request was throttled.' detail.

solid answer

~40 s

`DEFAULT_THROTTLE_CLASSES` is an empty list out of the box, so nothing is rate limited until you add, say, `AnonRateThrottle` and `UserRateThrottle`, and set `DEFAULT_THROTTLE_RATES` such as `{"anon": "100/day", "user": "1000/day"}`. A rate is `number/period`, and only the period's first letter matters (`s`, `m`, `h`, `d`). `AnonRateThrottle` counts only unauthenticated requests, keyed by client IP; `UserRateThrottle` counts authenticated requests by user primary key and falls back to IP for anonymous ones. The check runs in `initial()` after authentication and permissions. When a class refuses, DRF raises `Throttled`: status 429, a detail like "Request was throttled. Expected available in 42 seconds.", and a `Retry-After` header with the wait in whole seconds. A view's `throttle_classes` (or `@throttle_classes` on a function view) replaces the global list.

code

python · 14 lines
python
from rest_framework.decorators import api_view, throttle_classes
from rest_framework.response import Response
from rest_framework.throttling import AnonRateThrottle


class SearchAnonThrottle(AnonRateThrottle):
    rate = "20/min"


@api_view(["GET"])
@throttle_classes([SearchAnonThrottle])
def search(request):
    term = request.query_params.get("q", "")
    return Response({"query": term, "results": []})

go deeper

for a junior

Recall the two settings, the rate string format, and that a throttled request answers 429 with Retry-After.

for a middle

Explain how anon and user classes key their counters, why a partial DEFAULT_THROTTLE_RATES breaks, and where check_throttles() sits in initial().

for a senior

Decide which endpoints need their own limits, check that custom exception handlers keep Retry-After, and confirm rates survive settings changes.

for a principal

Place DRF throttling as a per-client fairness layer beneath edge limits, and choose where each kind of limit is enforced and owned.

## Throttling is off until you configure it Django REST Framework (DRF) calls rate limiting **throttling**. A throttle class decides, for each request, whether the client has used up its allowance. Two settings in the `REST_FRAMEWORK` dictionary drive it: - `DEFAULT_THROTTLE_CLASSES` — which classes every view runs. Its default is `[]`, so **a fresh project throttles nothing**. - `DEFAULT_THROTTLE_RATES` — the allowance for each **scope**. Its default is `{'user': None, 'anon': None}`, and a rate of `None` means the class allows every request. ```python # settings.py REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "anon": "100/day", "user": "1000/day", }, } ``` Because DRF merges its settings one top-level key at a time, this `DEFAULT_THROTTLE_RATES` dictionary replaces the default one completely: any scope a listed class needs must appear in it, or the class raises `ImproperlyConfigured` when a request arrives. ## Reading a rate string A rate is `"<number>/<period>"`. `SimpleRateThrottle.parse_rate()` splits on `/` and looks only at the **first character** of the period: | Period starts with | Window length | |---|---| | `s` | 1 second | | `m` | 60 seconds | | `h` | 3,600 seconds | | `d` | 86,400 seconds | So `"30/m"`, `"30/min"` and `"30/minute"` are identical. The window is not tied to the clock: DRF stores a list of timestamps per client and drops those older than the window on each request. ## What each built-in class counts - **`AnonRateThrottle`** (scope `anon`) — throttles only unauthenticated requests. For an authenticated user its `get_cache_key()` returns `None`, which means "do not throttle". The key is the client IP. - **`UserRateThrottle`** (scope `user`) — throttles everyone. Authenticated requests are keyed by `request.user.pk`; anonymous requests fall back to the client IP. - **`ScopedRateThrottle`** — applies a named scope only to views that set `throttle_scope`. Each class builds a cache key of the form `throttle_<scope>_<ident>` and keeps its history in Django's cache. ## Where the check runs `APIView.initial()` runs its policy steps in order: 1. `perform_authentication()` — so `request.user` is known when the user throttle needs its primary key; 2. `check_permissions()` — a request refused here never reaches the throttles; 3. `check_throttles()` — every throttle class is asked, and any refusal stops the request. ## What the client sees When a throttle refuses, the view raises `rest_framework.exceptions.Throttled`: - **status 429** (`HTTP_429_TOO_MANY_REQUESTS`); - a `detail` of "Request was throttled." plus "Expected available in N seconds." when a wait is known; - a **`Retry-After`** header with the wait rounded up to whole seconds, added by DRF's default exception handler whenever the wait is known. The wait is computed from the stored history — roughly, the time until the oldest recorded request leaves the window. With several throttles refusing at once, DRF reports the largest wait. A custom exception handler that builds its own response without calling DRF's default handler must copy `Retry-After` itself. ## Per-view control A class-based view sets `throttle_classes = [...]`; a function view uses `@throttle_classes([...])` under `@api_view`. As with permissions, the view's list **replaces** the global one, so `throttle_classes = []` exempts a view entirely.

  • What happens in DRF if UserRateThrottle is listed but DEFAULT_THROTTLE_RATES only defines 'anon'?
    Your dictionary replaces DRF's default one, so the `user` key is missing. When the throttle is instantiated for a request, `get_rate()` cannot find the scope and raises `ImproperlyConfigured` ("No default throttle rate set for 'user' scope"), which surfaces as a server error on every request. Define every scope that a listed class uses.
  • Does a DRF request refused by a permission class count against the throttle?
    No. `initial()` calls `check_permissions()` before `check_throttles()`, and a refusal raises straight away, so the throttles never record that request. Only requests that pass authentication and permissions are counted.

saying these in an interview costs you the question

  • DRF throttles anonymous clients out of the box.
  • AnonRateThrottle also counts requests from logged-in users.
  • A throttled DRF request gets 403 Forbidden.
  • '100/minute' is invalid because DRF only accepts 'm'.
  • Throttle windows reset on the wall clock, at the top of each minute or day.