Free tool
Six rate limiters, one minute of traffic.
Pick an algorithm, throw a burst, a scraper or a thundering herd at it, and watch what it serves and what it rejects — then copy the nginx, Express, NestJS or Redis config that implements exactly what you just watched. Nothing leaves your browser.
Choosing
Which one should you actually use?
Four questions, in the order that settles it.
Does a burst need to get through?
A mobile app that wakes up and syncs, a dashboard that fires eight calls on load, a client catching up after a network blip — these are legitimate and they all look like an attack to a flat rate limit. A token bucket lets a client that has been quiet spend what it accrued, which is usually what you want and why it is the default in almost every gateway.
Is the limit a promise you published?
“1,000 requests per hour” in a pricing table is a contract, and a fixed window will let a customer send 2,000 of them in a few minutes across a boundary. Use a sliding window counter for that, and a sliding window log only where the number has to be exact to the request — it costs one stored timestamp per allowed call, per client.
Is the thing downstream fragile?
A payment provider with its own limit, a legacy system with a fixed connection pool, a model endpoint you are billed per call for. Here you do not want to reject, you want to smooth. A leaky bucket gives you a dead-flat output rate and pays for it in latency — which is a trade you can only make if the caller can wait. Run the thundering herd above against it and read the worst queue wait.
Is the caller hostile?
A scraper does not back off on a 429; it retries. Against that, every algorithm above still answers one request per rejection, forever. An escalating lockout changes the economics: trip once and you lose a minute, keep coming and the windows grow 1, 1, 2, 3, 5, 8 until the client is gone for the afternoon — while an honest client that trips once barely notices. That is the shape I shipped over a 4 TB search corpus, and the write-up explains why Fibonacci rather than doubling.
Mistakes
Four ways a correct limiter still fails
None of these are algorithm problems, and all four are what actually breaks in production.
In-memory counters behind a load balancer
Four pods, each holding its own count, is four times the limit you published — and it moves every time you scale. The state has to be shared, which in practice means Redis, and the check has to be atomic, which means a Lua script rather than GET then INCR.
Keying on the wrong thing
An IP is shared by a whole office behind NAT and rotated for free by anyone you want to stop. Key on the API key or user where one exists. Behind a CDN, check what you are actually reading — a limiter keyed on your own load balancer's address rate limits the entire internet as one client.
A 429 with no Retry-After
A client that is not told when to come back comes back immediately. Rejections are cheap, but they are not free, and a retry storm against a limiter is still a load problem. Send Retry-After, and send the RateLimit headers so a well-behaved client can pace itself and never be rejected at all.
One limit for every endpoint
A search that fans out across a shard and a health check that returns a constant are not the same request, and a single number has to be generous enough for the cheap one. Budget by cost, not by call: a limit per route, or tokens spent in proportion to how expensive the work is.
Questions
Rate limiting, asked plainly
Which rate limiting algorithm should I use?
Token bucket, unless you have a reason. It absorbs the burst a quiet client has earned, it is one hash and two numbers per client, and every gateway already implements it. Reach for a sliding window when you publish an exact contract like 1,000 requests per hour and a client will hold you to it, for a leaky bucket when the thing you are protecting cannot take a spike at all, and for an escalating lockout when the caller is hostile rather than merely noisy.
What is the difference between a token bucket and a leaky bucket?
A token bucket limits the average rate but lets a client spend a full bucket at once, so bursts pass and rejection is immediate. A leaky bucket is a queue drained at a fixed rate: the output is perfectly smooth whatever arrives, and excess requests wait rather than fail. Token bucket protects your budget; leaky bucket protects a fragile downstream. The cost of the leaky bucket is latency — run the thundering herd pattern above and watch the worst-case queue wait.
Why is a fixed window rate limiter considered broken?
Because the counter resets on a clock boundary rather than relative to the client. A client that spends its whole budget in the last second of one window and again in the first second of the next has sent twice the limit inside a span shorter than one window. Set fixed window above with a limit of 60 per 10s and it will happily pass 120 requests in about a second. It is still the right choice when the limit is a rough guard rather than a promise, because it costs one integer.
Is a sliding window counter accurate enough?
Almost always. It weights the previous window's count by how far into the current one you are, which assumes that traffic was evenly spread — so it can be a few percent strict or lenient against a real burst. In exchange it costs two integers per client instead of one timestamp per allowed request. Only use a sliding window log when the exactness is contractual.
Should I rate limit by IP address?
Only as a last resort. An IP is shared by everyone behind a corporate NAT or a mobile carrier and is trivially rotated by anyone you actually want to stop. Key on the authenticated user or the API key wherever one exists, fall back to IP for anonymous traffic, and if you are behind a proxy or CDN, make sure you are reading the forwarded client address rather than rate limiting your own load balancer.
What should a rate limited response return?
429 Too Many Requests, with a Retry-After header carrying the seconds until the client may try again, and RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset so a well-behaved client can pace itself without being rejected at all. A 429 with no Retry-After teaches clients to retry immediately, which turns your limiter into the thing generating the load.
Where should the limiter live — the edge or the application?
Both, doing different jobs. The edge sheds volumetric load before it costs you a process, which is what nginx limit_req and a CDN rule are for. The application enforces the limit that is part of your API contract, because only it knows the authenticated principal and the plan they are on. Anything enforced per-instance in memory is not a limit — with four pods behind a load balancer you have quietly shipped four times the number you published.
Where this came from
Built from a limiter that had to hold
The escalating lockout above is not a textbook example. It ran in front of talent search over a 4 TB JSON corpus in Elasticsearch, where a single caller in a loop could degrade search for every other client on the platform.