Skip to main content

Rate Limiting Algorithms

Level 4: Level 4: Scaling Compute & Trafficmedium30 minrate-limitingtoken-bucket

Token bucket for burst-friendly limits, sliding-window counter for accuracy without the log's memory, never raw fixed window, and always a 429 + Retry-After contract.

The shape of what you allow

Rate limiting answers one question: given a stream of requests keyed by some dimension (per-user, per-IP, per-API-key, per-endpoint, or global), do I allow or reject the next one? The interesting part is the shape of what you allow. Two systems can both permit "100 requests per minute" and behave completely differently at the second scale.

Token bucket is the usual default because it is burst-friendly. A bucket holds up to B tokens and refills at R tokens per second. Each request removes one token; if the bucket is empty, reject. A client that has been idle accumulates up to B tokens, so it can fire a burst of B instantly, then settle to the steady rate R. You store just two numbers per key: the current token count and the last-refill timestamp, refilled lazily on each access (tokens = min(B, tokens + elapsed * R)). Capacity B sets the maximum burst; R sets the long-run rate. This is what most APIs want: allow natural bursts, cap sustained abuse.

Leaky bucket is the opposite intent: it smooths output. Requests enter a queue and drain at a fixed rate, so downstream sees a perfectly steady stream. Use it when the thing you protect cannot absorb bursts at all (a payment processor with a hard TPS ceiling). The cost is added latency and a queue to manage.

Check yourself
A limiter allows 100 requests per minute per key, counting in aligned one-minute windows that reset at :00. A client sends 100 requests at 11:00:59 and 100 more at 11:01:01. What gets through?

Fixed window counts requests per aligned interval and resets at the boundary. Trivially cheap (one integer per key per window), but it has the boundary spike: a client can send the full quota in the last second of one window and the full quota in the first second of the next, delivering 2x the intended rate across a two-second span.

Sliding-window log stores a timestamp per request and counts those in the trailing window: exact but memory-heavy (1000 req/min = 1000 stored timestamps). The practical compromise is the sliding-window counter: keep the current and previous fixed-window counts and weight the previous one by how much still overlaps the trailing window (count = current + previous * overlap_fraction). It kills the boundary spike with roughly the memory of fixed window.

Check yourself

Match each property to the algorithm it belongs to.

An idle client can fire a burst of B, then settles to rate R
Downstream sees a perfectly steady stream, at the cost of queueing latency
Weights the previous window by its overlap fraction to fix the boundary spike
Stores just a token count and a last-refill timestamp per key
The right pick in front of a payment processor with a hard TPS ceiling

Interview nuance: the response contract matters as much as the algorithm. On rejection return HTTP 429 Too Many Requests with a Retry-After header and the standard RateLimit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) so well-behaved clients back off. Decide fail-open vs fail-closed: if the limiter's state store is unavailable, do you allow traffic (protect availability, risk overload) or block it (protect the backend, risk an outage)? Most public APIs fail open on the limiter and rely on downstream load shedding as the real backstop.

token bucket (burst-friendly)        fixed window (boundary spike)
  cap B, refill R/sec                  [--59 reqs--|--59 reqs--]
  idle -> save up to B tokens                    ^ 118 in ~1s
  req  -> take 1 or 429                sliding-window counter fixes it

Recap: default to token bucket for burst-friendly per-key limits, use sliding-window counter when you need window accuracy without the log's memory, avoid raw fixed window for anything abuse-sensitive, and always return 429 plus Retry-After and RateLimit headers with a stated fail-open or fail-closed policy.

Check yourself
You are designing per-key limits for a public API: clients burst naturally, sustained abuse must be capped, window-edge doubling is unacceptable, and the limiter's state store can go down. Which combination fits?

Apply

Your turn

The task this lesson builds to.

Write the algorithm for an API rate limiter that allows short bursts but enforces a steady long-run rate, and state the counters it stores per user.

Think about

  1. Which algorithm allows bursts vs smooths output?
  2. What is the fixed-window boundary-spike bug?
  3. What is the client-facing response contract?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design the rate-limiting tiers for Stripe-style payment APIs where a single merchant may legitimately batch thousands of charges at midnight but a compromised key must be contained fast. Choose the algorithms per tier and justify.

Think about

  1. Why can one flat limit not serve both the nightly batch and the fraud case?
  2. What does an anomaly-triggered clamp add that static limits cannot?
  3. Why does the payment tier invert the usual fail-open default?