Rate limiting by IP: everything that goes wrong

Parneet Singh8 min read

Rate limiting by IP address is the first thing most people build and the thing most people build wrong. The idea is simple enough — count requests per address, reject above a threshold — and every part of that sentence hides a problem.

One address is not one user

This is the assumption everything else breaks against.

CGNAT. IPv4 addresses ran out, so many providers place large numbers of customers behind a single public address. An entire suburb can share one. A limit that seems generous per person is severe per address.

Corporate and campus networks. A university, a hospital, or any office of size egresses through a handful of addresses. Thousands of legitimate users, one bucket.

Mobile carriers. Same pattern, larger scale, and the address a handset uses can change mid-session as it moves between gateways.

The consequence: a per-IP limit that is strict enough to stop an attacker is often strict enough to break a shared network of ordinary users, and you will hear about it as "your site is broken at my office" rather than as a rate limit.

One user is not one address

The reverse fails too.

IPv6 makes addresses effectively free. A residential IPv6 allocation is commonly a /64 or larger — that is 18 quintillion addresses for one household. Rate limiting per IPv6 address is not rate limiting; an attacker simply uses a new address for every request.

For IPv6, limit on the prefix, not the address. A /64 is the usual unit, which is roughly "one customer network". Treating each /64 as one bucket restores the property you wanted.

Mobile addresses rotate. A user can pick up a new address by moving cell, or by toggling airplane mode.

Rotation is cheap for anyone who wants it

Residential proxy services sell access to large pools of genuine consumer addresses. Cloud providers rent addresses by the hour. Somebody determined to get past a per-IP limit will, and it will not be difficult.

This does not make IP rate limiting useless. It makes it a cost multiplier rather than a barrier: it raises the price of abuse and stops the unsophisticated majority. Treat it as one layer, not the layer.

Things that are usually wrong

Trusting X-Forwarded-For blindly. It is client-supplied and forgeable. If you rate limit on the leftmost value without validating which proxy appended it, you have given the caller control of their own bucket identity. Configure trusted proxies, take the address your proxy added, and fall back to the socket address.

Counting in a fixed window. A limit of 100 per minute implemented as a counter reset on the minute allows 200 requests in two seconds across the boundary. Use a sliding window or a token bucket.

Rate limiting after the expensive work. A limiter that runs after authentication, database queries or a third-party call has already spent the resource it was protecting. Put it first.

Returning the wrong status. Use 429 Too Many Requests with a Retry-After header. 403 tells a client the request will never succeed, which is a different and wrong instruction.

Silent limiting. Publish the limit and report the remaining budget in headers. A client that can see its budget will back off; a client that hits an opaque wall will retry.

A more workable shape

Layer the identity you count against, from most to least trustworthy:

IdentityWhen it appliesNotes
API key or accountAuthenticated requestsThe only identity that is actually stable
Session or deviceLogged-in web trafficSurvives address changes
IPv6 /64 prefixAnonymous IPv6Not the individual address
IPv4 addressAnonymous IPv4Assume it may be many people

Anonymous traffic gets the loosest limit and the least trust. Authenticated traffic gets a real quota, because there is finally something meaningful to count against.

That is the shape used here: 45 requests a minute with no key at all, then a real monthly quota once you sign up and there is an account to attach it to. The keyless limit exists to keep the service usable without an account while making bulk abuse tedious — not to stop a determined attacker, which it could not.

Where the datacenter signal helps

Anonymous traffic from a datacenter address is not the same as anonymous traffic from a residential one. It is not necessarily malicious — plenty of legitimate integrations run on servers — but it is a different population, and it is reasonable to apply a different limit to it.

{ "privacy": { "hosting": true, "tor": false } }

This is a far better input to a rate limiter than trying to guess from request patterns, and it is cheap to check.


Every lookup here returns the privacy block alongside the location, so you can use it as a rate-limiting input — see the API documentation. If you are working with address ranges rather than single addresses, the IP-in-range checker and CIDR tools handle the arithmetic.

More on engineering

All posts · RSS