retrystorm · Learn · Interview practice · Glossary · Components

Home › Learn › Rate Limiting

Chapter 10 · Foundations · 40 min

Rate Limiting

Per-caller limits: token buckets, fixed and sliding windows, 429s, limits across many machines, and how they differ from load shedding.

Inkwell opened a public API, and partners started building on it. This morning one of them shipped a bug that calls the API in a loop, at more than twice what its contract allows, and every partner's requests are failing, including the ones that did nothing wrong.

Load shedding turns requests away when the servers are full, whoever sent them. A rate limiter decides in advance how much each caller may send, so one caller's flood can't take the system away from everyone else.

One caller can take everything

When traffic is more than the servers can take, every caller suffers the overload together. It doesn't matter that one caller sends four times its share and the others behave: their requests wait in the same queues and fail in the same timeouts.

Try one limit for everyone and one limit per client, on a noisy client and a quiet one. Watch which of the two gets refused.

What a rate limit is

A rate limit says: this caller may send at most this many requests per second (or minute, or day). The limiter counts each caller's requests, by a key such as an API key, a user id or an IP address, and refuses the ones over the limit straight away, with HTTP 429 Too Many Requests and usually a Retry-After header saying when to try again.

Limit byFair toWeak against
API keyeach partner or appone key shared by many users
User ideach signed-in persontraffic before login
IP addressanonymous trafficmany users behind one office or phone network

A refused request costs the limiter a fraction of a millisecond, and the app servers nothing. Callers that retry a 429 right away just send more traffic to refuse, so good clients honour Retry-After and back off.

Token bucket

The most common algorithm. Each caller has a bucket that holds up to burst tokens and refills at limit tokens a second. Each request takes a token; with none left, it's refused.

Bucket of 50, refilling at 100 a second
A caller idle for a whilecan send 50 at once, then 100 a second
A caller sending 100 a second, steadilyis always let through
A caller sending 300 a secondgets 100 a second through, and 200 refused

The burst lets normal, bursty callers through without trouble while the long-run rate stays capped. It needs two numbers per caller: the tokens left and when they were last refilled.

Bursts of 60 requests arrive below, against a limit of 100 a second. The bucket size decides how much of each burst gets through.

A leaky bucket is the token bucket's mirror. Requests join a queue of the same size, and leave it at a fixed rate. Nothing is refused until the queue is full, and what comes out the other end is steady however bursty what went in was. The price is the wait in the queue.

Fixed and sliding windows

A fixed window counts requests per calendar second (or minute) and resets the count when the next one starts. It's the simplest to build: one counter per caller per window. Its flaw is at the boundary: a caller can send the whole limit at the end of one window and again at the start of the next, twice the limit in a moment.

A sliding window counts the requests in the last full window, wherever it ends, so no span of that length ever holds more than the limit. Keeping every request's time (a log) is exact but costs memory; a weighted mix of the current and previous window's counts is a close, cheap approximation.

The limit is 100 a second below, and a client sends 100 requests just before a second ends and 100 just after.

AlgorithmAllows burstsMemory per callerAt the boundary
Token bucketup to the bucket sizetwo numbersno boundary; up to the limit plus the bucket in any second
Fixed windowthe whole limit at onceone counterup to twice the limit
Sliding window (log)the whole limit at onceone time per requestexact
Sliding window (counter)the whole limit at oncethree numbersa little over (up to about 19% in the labs)

Limits on many machines

A limiter usually runs on every load balancer or API gateway node (the front door that checks and routes API calls), and a caller's requests are spread over all of them. If each of 4 nodes allows 1 000 a second on its own, the caller really gets 4 000.

ApproachHowCost
Split the limiteach of 4 nodes allows 250uneven spreading refuses some requests early
Shared counterevery node counts in one store, such as Redisan extra network call per request, and a store that must stay up
Local, synced now and thencount locally, share totals every few hundred mscallers can briefly go over

A shared counter also needs a plan for when its store is down: fail open (let everything through, with no limits) or fail closed (refuse everything). Most limiters fail open, since a short spell without limits hurts less than refusing every caller.

Four nodes share one client's traffic below, with most of it landing on one node, as real traffic often does.

Rate limiting or load shedding

The two work together. A rate limit is a promise per caller, set ahead of time, and enforced even when the servers have room. Load shedding is the last line, for when the servers are full whatever the callers are doing.

Rate limitingLoad shedding
Decides bywho is askingwhether there's room right now
Protectsother callers, and the billthe servers
Refuses with429 Too Many Requests503 Service Unavailable

In the wild

The ideas in this chapter, and where you'll meet them in real systems and interviews:

IdeaWhere you'll meet it
API rate limitsGitHub (5 000 requests an hour per token), Stripe, Twitter's API, all answering 429
Limiters in front of serversAWS API Gateway throttling, NGINX limit_req, Envoy's local and global rate limits, Kong
Shared countersRedis INCR with EXPIRE for fixed windows, Redis sorted sets for sliding logs
A classic interview question"Design a rate limiter": the algorithms, the key, and where the counts live

Your practice

Inkwell's partners build on its public API, and every contract allows 1 000 requests a second. One partner shipped a bug that calls the API in a loop at more than twice that, and every partner's requests are failing. Put a limiter in front of the API so the well-behaved partner never notices, and the buggy one gets exactly what its contract allows.

Key takeaways

  • A per-client limit is fair: quiet callers never notice; a global one refuses everyone alike.
  • Limit by a key (API key, user, IP) and refuse fast with 429 and Retry-After.
  • A token bucket lets bursts up to its size through; a leaky bucket queues them and sends a steady stream.
  • A fixed window can let twice the limit through at a boundary; a sliding window keeps any span under the limit.
  • On many nodes, split the limit or share a counter, or each node's limit adds up.

Read first

  • Load Shedding

Practise it

  • Fair Share: Keep one partner's runaway integration from taking Inkwell's API away from everyone else.

Terms

  • Fail open and fail closed: What a limiter does when its shared store is down: let everything through, or refuse everything.
  • Fixed window: Counts requests per calendar second or minute; simple, but lets up to twice the limit through at a boundary.
  • HTTP 429: Too Many Requests: the answer to a caller over its rate limit, usually with a Retry-After header.
  • Leaky bucket: A rate limiter that queues requests and lets them out at a fixed rate, refusing only when the queue is full.
  • Rate limiter: Caps how many requests each caller may send, refusing the rest at once with HTTP 429.
  • Sliding window: Counts the requests in the last full window wherever it ends, so no span holds more than the limit.
  • Token bucket: A rate-limiting algorithm: tokens refill at the limit, each request takes one, and the bucket size allows short bursts.
About · Contact · Privacy · Terms of use