Back to the map

Module 04 · API fundamentals · Lesson 10 of 15

Rate limiting

Limits protect the system from any one caller. Token bucket, leaky bucket, fixed and sliding windows compared, where limits live, how to keep them consistent across servers, and how to respond so clients back off well.

CoreMust-knowMid 12 minVideo Updated Nov 2026
Video walkthrough · 2:48 · narrated, with captions

Why it matters

Without limits, one misbehaving script, one retry storm or one abusive tenant takes the whole API down for everyone. Rate limiting bounds what any single caller can consume, so the system degrades for the offender instead of for all. It is also a product surface: tiers, quotas and fair use are expressed as limits. It appears in almost every design question because it is the first line of defence and the simplest form of back-pressure.

The idea

A rate limiter decides, per key (user, API key, IP, tenant, endpoint) whether a request may proceed now, and if not, how long to wait. The algorithms differ in how they handle bursts and how much state they keep.

The algorithms

AlgorithmHow it worksBurstsMemory per keyNotes
Token bucketBucket holds up to B tokens, refilled at R per second; a request takes one token or is rejectedAllowed up to B, then smoothed to RTwo numbers: tokens, last refill timeThe default choice; simple, burst-tolerant, cheap
Leaky bucketRequests queue in a bucket that drains at a constant rate; overflow is rejectedSmoothed completely; output is steadyQueue lengthShapes traffic rather than just gating it; adds latency
Fixed windowCount requests per calendar window (per minute); reject above NUp to 2N at a window boundaryOne counterSimplest; boundary burst problem
Sliding window logStore a timestamp per request; count those within the last windowExactOne entry per requestPrecise but memory-heavy at high rates
Sliding window counterWeight the previous window's count by its overlap with the current oneApproximately exactTwo countersGood precision at fixed-window cost; common in practice
RequestsBucketcapacity B · refill R/s+R tokens / stoken: allowempty: 429200429
Token bucket: capacity B absorbs a burst, then the refill rate R sets the steady throughput. Requests arriving at an empty bucket get 429.

Where the limiter lives

At the edge (API gateway, CDN, reverse proxy) to shed abusive traffic before it costs anything. Per service for limits that depend on business context, such as a tenant's plan. Inside clients of downstream dependencies, to stay under a third party's quota. Most systems have all three.

Making it consistent across servers

A counter in one server's memory limits only that server's share. For a global limit, keep state in a shared store, typically Redis, and make the read-modify-write atomic with a Lua script or INCR plus EXPIRE so concurrent servers do not both admit the last request. Keys are limit:{tenant}:{endpoint} with a TTL matching the window. For very high rates, accept approximation: each server limits locally against its share of the global quota, or syncs counts asynchronously, trading exactness for a shared store that is no longer a hot spot.

Responding well

Return 429 Too Many Requests with a Retry-After header saying when to try again, and expose RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers on every response so clients can pace themselves before hitting the wall. Document limits per tier. Distinguish rate limits (requests per second) from quotas (requests per month) in the same headers. Clients should back off exponentially with jitter, which the reliability lessons in later stages revisit.

Keys and fairness

Limit by the identity you can trust: an authenticated user or API key where available, IP as a fallback with care (NAT puts many users behind one address). Separate limits for expensive endpoints (search, export) from cheap ones. Consider cost-based limiting, where each request consumes a weight, for APIs with wildly varying operation costs, including GraphQL.

Trade-offs

Token bucket is the sensible default; fixed windows are trivial but allow double bursts at boundaries; sliding logs are exact and expensive; leaky buckets smooth output at the cost of queueing latency. Centralised state gives exact global limits and adds a dependency and a round trip; local limiting is cheap and approximate. Tight limits protect the system and frustrate legitimate heavy users; the answer is usually tiers, not one number.

In an interview

Give algorithm, key, store and response in one breath: "Token bucket per API key, 100 requests per second with a burst of 200, state in Redis updated atomically with a Lua script, enforced at the gateway. Over limit returns 429 with Retry-After, and every response carries remaining and reset headers." Then the nuance: "Search and export have their own lower buckets because they cost far more than reads."

Check yourself

A fixed-window limiter allows 100 requests per minute. A client sends 100 requests at 12:00:59 and 100 more at 12:01:00. What happened, and which algorithm prevents it?

Rate limiting · Backplane