Rate limiting by IP: everything that goes wrong
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:
| Identity | When it applies | Notes |
|---|---|---|
| API key or account | Authenticated requests | The only identity that is actually stable |
| Session or device | Logged-in web traffic | Survives address changes |
| IPv6 /64 prefix | Anonymous IPv6 | Not the individual address |
| IPv4 address | Anonymous IPv4 | Assume 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
What we found in 134,000 cloud provider IP ranges
We measured how often ASN-based hosting detection misses cloud addresses. Six providers scored under 2.3%. AWS missed 15.3%.