Concepts

Shaping traffic

Traffic

Rate Limiting

Deciding whether this request is allowed through, and what a burst is permitted to do.

Every algorithm trades memory per key against how accurately it handles bursts.

token bucketsliding windowbursts

Try it

Move the dials — the sentence under the picture changes.
Algorithm
Traffic
window 1 startswindow 2 startswindow 3 startsallowed — 20 of 24rejected — 4limit: 10 per 20 ticks · peak actual: 20
The counter resets at the window boundary, so a client that spends its whole allowance just before the reset gets a fresh one immediately after. 20 requests got through in one 20-tick stretch against a limit of 10 — up to 2× the intended rate, and the arithmetic was never violated.

In plain words

A rate limiter decides, for every request, whether this caller has done too much recently — and says "not now" if so. It is the bouncer with a clicker: so many people per minute, and the rest wait outside. It protects your system from abuse, from a buggy client stuck in a loop, and from itself.

The algorithms, judged by what they do to a burst

Under steady traffic below the limit every algorithm behaves the same. They differ in how they treat a burst — a quiet client that suddenly sends a lot.

59:5012:0012:10Fixed window, limit 100 per minute. Client is quiet…11:59:58 — spends all 100 in two seconds. Allowed: the 11:59 window had room.12:00:01 — new window, fresh 100. Another 100 in two seconds. Allowed.200 requests in four seconds under a '100 per minute' limit. Nothing was violated.
The fixed-window edge case: a client can double the limit by straddling the boundary.
Fixed window
know the flaw

One counter per key per calendar minute. Cheapest possible — and the boundary trick above lets a client do 2× at the edge.

Sliding window log
exact, unusable

Store the timestamp of every accepted request; count the ones in the last 60 s. Exactly right, but memory grows with traffic — the busiest clients, the ones you most want to limit, cost the most to track.

Sliding window counter

Keep this minute's count and last minute's; blend them by how far into the current minute you are. Two numbers per key, and the boundary trick stops working. Slightly off when traffic within a minute is very lumpy.

Token bucket
usual answer

A bucket holds up to N tokens and refills at a steady rate; each request spends one. A quiet client has a full bucket and may burst, then settles to the refill rate. That matches how real clients behave.

Leaky bucket

Requests queue up and drain at a fixed rate. Perfectly smooth output for a downstream that cannot take bursts — bought by queueing requests, which adds latency.

Implementing a token bucket

The trick that makes it cheap: do not run a timer that adds tokens. Work out how many tokens would have arrived since the last request, when the next request comes in.

Token bucket, refilled lazilyPython
import time

class TokenBucket:
    def __init__(self, capacity: int, refill_per_sec: float):
        self.capacity = capacity          # burst size
        self.refill = refill_per_sec      # steady rate
        self.tokens = capacity
        self.updated = time.monotonic()

    def allow(self) -> bool:
        now = time.monotonic()
        # tokens that arrived while we were not looking
        self.tokens = min(self.capacity, self.tokens + (now - self.updated) * self.refill)
        self.updated = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

Two numbers per key (tokens, updated), one small calculation, no background work. Burst size and refill rate are separate dials: "up to 20 at once, 100 a minute sustained".

Making it correct across many servers

Ten API servers each running the bucket above, each allowing 100 a minute, allow 1,000 a minute in total. The count has to be shared.

Shared counter in Redis
usual

Every server increments the same key. Correct, one round trip per request, and Redis becomes a hard dependency.

Local counts, synced

Each server limits to its share and swaps totals with the others every few hundred ms. No round trip, survives Redis being down, briefly lets a little extra through. Fine for abuse protection; wrong for a paid quota.

Route each key to one server

Hash the API key at the load balancer so it always lands on the same server; local counts are then exact. Uneven load, and a mess whenever servers come and go.

The check must be atomic — done in one indivisible step. GET the count, decide, SET it back is a race: two requests read 99 at the same instant and both pass. Use a single command, or a small script Redis runs as one unit.

Sliding window counter, atomically, in RedisRedis
-- KEYS[1] = ratelimit:{api_key}:{minute}   ARGV[1] = limit
local n = redis.call('INCR', KEYS[1])        -- count and read back in one step
if n == 1 then
  redis.call('EXPIRE', KEYS[1], 120)         -- keep this minute and the previous
end
if n > tonumber(ARGV[1]) then return 0 end   -- over: reject
return 1

What to send back

  1. Reject with 429 and say when to come back

    429 Too Many Requests with a Retry-After header. Add a little randomness to the value, or every rejected client retries in the same second.

  2. Tell clients how much room they have — on every response

    X-RateLimit-Limit: 100, X-RateLimit-Remaining: 37, X-RateLimit-Reset: 1726300800. A client that can see the number slows down before it is rejected. This, more than the algorithm, is what produces well-behaved clients.

  3. Decide what happens when the counter store is down

    Fail open — let traffic through and page someone — unless the limit is about money or correctness (a metered API), in which case fail closed. A limiter that takes the service down to protect it has failed at its job.

HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1726300823

Where it goes wrong

  • Keying on IP. Everyone behind one office or mobile carrier address (NAT) shares a bucket; an attacker with many addresses gets many buckets. Key on the API key or user; IP is a last resort for anonymous traffic.
  • Hot keys. One huge customer's counter is one Redis key on one shard. Split their key by server (key:{server}) and sum, or give them a bucket per region.
  • Many limits per key. 100/min, 5,000/hour and 50,000/day is three counters. Send the three commands in one batch (a pipeline) so it is still one round trip.
  • Retry storms. Every rejected client retrying at exactly Retry-After is a synchronised wave — see retries and backoff.

Take this with you

  • The one idea: judge a limiter by what it does to a burst. Steady traffic under the limit looks the same under every algorithm.
  • In an interview, pick token bucket, explain the fixed-window edge case, and say where the counter lives across many servers.
  • At work, send the limit headers on every response, and know whether your limiter fails open or closed when its store is down.