Behind a reverse proxy, a DRF search endpoint's AnonRateThrottle either lumps all clients into one bucket or is dodged with forged X-Forwarded-For headers — what does NUM_PROXIES change?
answer
- which address identifies the client
- REMOTE_ADDR is the last hop
- the default trusts the whole header
- count trusted hops from the right
basics
~20 sDRF identifies anonymous clients in BaseThrottle.get_ident(). With NUM_PROXIES unset it uses the whole X-Forwarded-For string, which clients can vary; set it to your proxy count so DRF takes the address that many entries from the right.
solid answer
~40 sAnonymous throttling keys on `BaseThrottle.get_ident()`. With `NUM_PROXIES` at its default `None`, DRF uses the **entire** `X-Forwarded-For` value, whitespace removed, when the header exists, and `REMOTE_ADDR` otherwise. That gives both failures: if the proxy does not forward the header, `REMOTE_ADDR` is the proxy, so every client shares one bucket; if it does, a client can prepend a random fake address each time and get a fresh key per request. Setting `NUM_PROXIES = n` makes DRF split the header and take the `n`-th entry from the right, which is the address your own outermost proxy saw; `NUM_PROXIES = 0` always uses `REMOTE_ADDR`. The count must match your real proxy chain: too high picks a client-supplied entry, too low picks a proxy address. Clients behind one NAT gateway still share a key.
code
python · 9 lines# settings.py
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {"anon": "60/min"},
# Edge proxy + load balancer, each appending to X-Forwarded-For.
"NUM_PROXIES": 2,
}go deeper
Know that DRF identifies anonymous clients by IP and that a proxy changes which address Django sees.
Explain get_ident(): the None default using the whole header, 0 meaning REMOTE_ADDR, and n meaning the n-th entry from the right.
Diagnose a shared bucket or a bypassed limit from logs, set NUM_PROXIES to the real chain, and close direct access to the app port.
Decide which limits belong at the edge and which in DRF, and who owns keeping the proxy count correct as infrastructure changes.
## How DRF names an anonymous client Django REST Framework (DRF) throttles anonymous requests by client address. `AnonRateThrottle`, and `UserRateThrottle` or `ScopedRateThrottle` for anonymous callers, all build their cache key from `BaseThrottle.get_ident(request)`. Two request values feed it: - `REMOTE_ADDR` — the address of whatever opened the TCP connection to the application server. Behind a reverse proxy, that is the proxy. - `X-Forwarded-For` (XFF) — a comma-separated list that proxies extend with the address they received the request from. Its left-hand entries come from whoever sent the request first, **including the client itself**, so they can be forged. ## The default: `NUM_PROXIES = None` `rest_framework/settings.py` ships `NUM_PROXIES: None`. With that value `get_ident()`: 1. returns the **whole** XFF string with whitespace removed if the header is present; 2. otherwise returns `REMOTE_ADDR`. Behind a proxy this fails in one of two ways, and which one depends on the proxy: | Proxy behaviour | Identifier DRF uses | Result | |---|---|---| | does not add XFF | the proxy's address | **every** anonymous client shares one bucket; one scraper locks out everyone | | adds XFF | the full header text | a client sending `X-Forwarded-For: <random>` gets a new key per request and is never throttled | The second case is the dangerous one for a public search endpoint: the limit looks configured and simply does not bind the clients it was meant for. ## Setting `NUM_PROXIES` `NUM_PROXIES` is the number of proxies you operate in front of Django. DRF then trusts only the entries those proxies wrote: - `NUM_PROXIES = 0` — ignore XFF and always use `REMOTE_ADDR` (right when Django is reached directly). - `NUM_PROXIES = n > 0` — split XFF on commas and take `addrs[-min(n, len(addrs))]`, the `n`-th entry from the right, stripped of spaces. Worked example with two proxies, an edge proxy and a load balancer, each appending the address it received the connection from: 1. The client sends `X-Forwarded-For: 203.0.113.99` (forged). 2. The edge proxy appends the real client: `203.0.113.99, 198.51.100.7`. 3. The load balancer appends the edge proxy: `203.0.113.99, 198.51.100.7, 10.0.0.5`. 4. With `NUM_PROXIES = 2`, DRF takes the second from the right: `198.51.100.7` — the real client. ## Getting the count wrong - **Too high** — DRF reads further left, into entries the client wrote, and forged headers work again. - **Too low** — DRF reads one of your own proxies' addresses, and many clients collapse into one bucket. - **Requests that bypass the proxies** — a shorter header makes DRF fall back to the leftmost entry, so the application port should not be reachable except through the proxy chain. - **NAT** — the DRF docs note that all clients behind one NAT gateway are treated as a single client once `NUM_PROXIES` is set; an office or a mobile carrier can share a key. ## Operating advice - Set `NUM_PROXIES` explicitly in every deployed environment; the default is only correct when there is no proxy and no XFF. - Log `get_ident()` for a few requests after each infrastructure change and check it against a known client address. - For authenticated traffic prefer `UserRateThrottle`, which keys on `request.user.pk` and is immune to address games. - Treat address-based throttling as a fairness control. The DRF documentation states that malicious actors can always spoof IP origins, so abuse protection belongs in front of Django as well.
- Why doesn't setting NUM_PROXIES help DRF's UserRateThrottle for logged-in users?For authenticated requests `UserRateThrottle` keys on `request.user.pk`, not on an address, so proxies and forged headers do not affect it. `NUM_PROXIES` only matters for the anonymous branch of the user and scoped throttles and for `AnonRateThrottle`.
- In DRF, what does NUM_PROXIES = 0 do when requests arrive with an X-Forwarded-For header?`get_ident()` returns `REMOTE_ADDR` whenever `NUM_PROXIES` is 0, ignoring the header entirely. That is correct when Django is reached directly, and wrong behind a proxy, where `REMOTE_ADDR` is the proxy and all clients share one bucket.
saying these in an interview costs you the question
- DRF ignores X-Forwarded-For unless NUM_PROXIES is set.
- With NUM_PROXIES unset, DRF uses the leftmost X-Forwarded-For address.
- A larger NUM_PROXIES is always the safer choice.
- REMOTE_ADDR is the real client address behind a load balancer.
- Once NUM_PROXIES is right, IP throttling stops determined attackers.