What Is the Difference between Rate Limiting and Throttling and Quotas
Under the hood all three are the same move: before a request is served, a counter tied to the caller is checked against a limit, and the request either decrements it or is refused. What separates them is only two knobs — how long the window is, and what happens on overflow. Rate limiting uses a short window (seconds) and rejects the overflow; throttling reacts to load by delaying or queuing it instead of dropping it; a quota uses a long window (a day, a billing month) and blocks until the period resets. Everything else on this page is a consequence of those two knobs. So the real content is the counter: the algorithm that decides, on every single request, whether a token is available.
The mechanism: a token bucket
The dominant rate-limiter algorithm (used by AWS API Gateway, Stripe, Envoy; NGINX's limit_req uses the closely related leaky bucket) is the token bucket. Picture a bucket with three parameters:
- Capacity C — the most tokens the bucket can hold. This is your maximum burst size.
- Refill rate r — tokens added per second. This is your sustained rate.
- Cost — tokens a request consumes (usually 1).
The bucket refills continuously toward C. Each request that arrives tries to remove a token: if at least one is present the request is served and a token is spent; if the bucket is empty the request is refused (429). Two numbers per client — current token count and the timestamp of the last refill — capture the entire state. The key insight: a client that has been quiet accumulates a full bucket and can fire C requests instantly (a burst), then is throttled down to the steady rate r once the bucket drains. That is exactly the "short bursts above the steady rate" behaviour the naive counter cannot express.
The whole algorithm, lazily refilled on each request (no background timer needed):
# Per client: state = {tokens: float, last: timestamp}
# CAPACITY = 10 tokens, REFILL_RATE = 5 tokens/sec
def allow(client, now):
b = state[client]
elapsed = now - b.last
b.tokens = min(CAPACITY, b.tokens + elapsed * REFILL_RATE) # accrue
b.last = now
if b.tokens >= 1:
b.tokens -= 1
return True # 200 OK (or, if throttling: delay then serve)
return False # 429 Too Many Requests
Note the refill is computed from elapsed time, not ticked by a clock — that is what keeps it O(1) memory and lock-light per client.
Worked trace: C = 10, r = 5 tokens/sec
The bucket starts full (10 tokens). Watch a burst drain it, watch requests get refused, and watch tokens re-accrue at 5/sec. Each request costs 1 token.
| Time | Event | Refill accrued (Δt × 5) | Tokens before | Decision | Tokens after |
|---|---|---|---|---|---|
| t = 0.0s | bucket starts full | — | — | — | 10.0 |
| t = 0.0s | 8 requests arrive at once | +0.0 | 10.0 | all 8 served (this is the burst) | 2.0 |
| t = 0.0s | 3 more requests, same instant | +0.0 | 2.0 | 2 served, 1 rejected (429) | 0.0 |
| t = 0.5s | 1 request | +2.5 | 2.5 | served | 1.5 |
| t = 1.0s | 4 requests | +2.5 | 4.0 | all 4 served | 0.0 |
| t = 1.1s | 1 request | +0.5 | 0.5 | rejected (0.5 < 1) | 0.5 |
In the first 1.1 seconds the client got 15 requests served. A dumb "5 per second" rule would have allowed roughly 5–6. The extra 9 came out of the pre-filled bucket — bounded burst, then hard convergence to the sustained 5/sec. That single behaviour is why token bucket won.
How the three differ, concretely
The distinction is not the algorithm — a token bucket can power all three — it is window length and overflow policy.
| Aspect | Rate limiting | Throttling | Quota |
|---|---|---|---|
| Window | seconds – minutes | real-time, reacts to load | day / week / billing month |
| Overflow action | reject (429) | delay / queue, serve later | block until reset |
| Purpose | fairness, abuse & DoS defence | keep the service alive under a spike | billing tiers, capacity planning |
| Counter storage | in-memory, resets constantly | in-memory + a queue | durable (survives reboots) |
| Enforced at | API gateway / proxy | app or infra (load balancer) | account / subscription |
| Example | GitHub: 60 req/hr unauthenticated | MS Graph “throttling”: 429/503 + Retry-After (client-side delay) | Free tier: 1,000 calls/month |
Terminology honesty: vendors do not use these labels consistently — Microsoft calls Graph's 429-with-Retry-After “throttling” even though by this page's taxonomy that is rejection-style rate limiting (the delay is pushed to the client via Retry-After rather than queued server-side). In an interview, define your terms by the two knobs (window, overflow action) and the ambiguity disappears.
They compose: a real gateway runs a per-second token bucket (rate limit), spills the overflow into a short queue on a spike (throttle), and separately tallies a monthly counter per API key (quota). GitHub's 60-req/hour unauthenticated limit is a real, correct example of the rate-limiting knob.
Pitfalls
- The fixed-window boundary burst (why the naive counter is wrong). A tempting implementation is "count requests, reset the counter every 60s." With a 100/min limit a client sends 100 at 12:00:59 and 100 more at 12:01:00 — 200 requests in one second, double the intended rate, because the two bursts land in adjacent windows. Token bucket and sliding-window algorithms don't have this seam; fixed-window counters do. Fix it by using a sliding window or a token bucket.
- Throttling with an unbounded queue. "Don't drop — just delay" quietly turns into a memory leak and latency blow-up: under sustained overload the queue grows without bound, tail latency explodes, and you fail slower instead of faster. Bound the queue and shed load past it.
- Limiting on the wrong key. Rate-limiting by client IP punishes everyone behind a corporate NAT or mobile carrier as if they were one abuser, while a botnet with thousands of IPs sails through. Limit by API key / user / token when you can.
- Per-node buckets in a fleet. Each server keeping its own in-memory bucket means the real limit is N× your intended one behind a load balancer. Global limits need a shared store (Redis) or a coordinated token-distribution scheme — at the cost of a network hop per request.
- Silent 429s with no guidance. Reject without a
Retry-Afterheader or remaining/reset headers and well-behaved clients hammer you in a tight retry loop, amplifying the overload. Always tell the client when to come back, and have them add jitter. - Non-durable quota counters. Quotas span a billing month; if the counter lives only in memory a reboot resets it and users get free calls (or, worse, you double-bill). Quotas need persistent storage; rate-limit counters can be ephemeral.
When to use which — and which algorithm
Two decisions: which overflow policy, and which counting algorithm.
Reject vs. delay vs. block
- Reject (rate limit / 429) when the caller can retry and you want back-pressure pushed to the client — interactive public APIs. Cheapest for the server (no state held). Choose this by default.
- Delay (throttle) when dropping is worse than waiting and you partly control the client — async/batch pipelines, protecting a fragile downstream during a flash sale. You gain graceful degradation; it costs server memory (queues), added tail latency, and queue-management complexity. Prefer rejection when clients are untrusted or numerous.
- Block over a long window (quota) for business/billing enforcement, not real-time protection. It costs durable storage and can't defend against a one-second spike — always pair it with a rate limit.
Counting algorithm
- Token bucket — choose when you want to permit bounded bursts and a smooth sustained rate; O(1) state (two numbers) per client. Trade-off: it does not strictly cap requests in an arbitrary window (a full bucket plus refill can exceed a naive per-window count). This is the right default for most APIs.
- Leaky bucket — choose when a downstream needs a strictly constant output rate (e.g., a queue drained at fixed speed): it converts bursts into steady drip. Costs added latency and disallows the burst that token bucket permits. Prefer token bucket when clients legitimately spike.
- Fixed-window counter — choose only for cheap, rough limits where precision doesn't matter; O(1) and trivial. Costs the 2× boundary-burst error above. Prefer token bucket or sliding window when the limit must be honest.
- Sliding-window log — choose when you need an exact count over any trailing window (strict compliance/security). Costs O(N) memory: one timestamp per request. The sliding-window counter approximates it at O(1) and is usually the better trade.
Rule of thumb: reach for a token bucket rate limiter as the default; add a durable quota for monetization; add throttling only for the specific downstream you must protect from spikes; and never ship a fixed-window counter where the boundary burst would actually hurt.
Takeaways
- All three are a counter checked before serving; they differ only in window length and overflow policy (reject / delay / block).
- The token bucket's two parameters map directly to intent: capacity = max burst, refill rate = sustained rate. Empty bucket → refuse.
- A naive fixed-window counter allows 2× the limit at window boundaries — a real bug; token bucket and sliding windows avoid it.
- Rate-limit for fairness (default: reject), throttle to survive spikes (delay a specific downstream), quota for billing (durable, long-window) — and compose all three at the gateway.
L0 · a limiter is just a counter with a window and an overflow policy — reject, delay, or block
L1 · ① Concurrency — "how do you enforce ONE global limit across a fleet of 200 stateless app servers?"
Trap: "each instance runs its own token bucket in memory — simple, no network hop."
Bar: a per-node bucket means 200 nodes each honoring 100 req/s admits 20,000 req/s fleet-wide — the limit is silently multiplied by N. Fix: move the counter to a shared store (Redis) and make check-and-decrement one atomic operation (a Lua script or `INCR` + conditional `EXPIRE`), never a separate GET-then-SET — otherwise two concurrent requests both read "9 tokens left" and both proceed, blowing past capacity. Rate Limiting Algorithms — Token Bucket, Sliding Window & Distributed
L2 · ② Failure — "your central Redis counter store goes down mid-traffic. What happens to the API?"
Trap: "fail closed and reject every request until Redis is back — safest, never over-admit."
Bar: failing closed turns a rate limiter into a single point of failure that takes the whole API down on a Redis blip; instead wrap the Redis call in a circuit breaker and fail open onto a conservative local token bucket per node, accepting a brief window of looser (not zero) enforcement until the store recovers. This is exactly the sticky-local-vs-global tension: global is correct but fragile, local is available but imprecise, so production systems degrade from one to the other rather than picking once. System Design Trade-offs II — Retry Storms, Rate-Limiter Coordination
L3 · ③ Scale — "10M req/sec across 5,000 nodes; a single Redis INCR per request won't keep up. Now what?"
Trap: "vertically scale Redis / add more replicas and keep doing INCR per request."
Bar: shard the counter keyspace across many Redis instances (hash by client key, same trick as consistent hashing for cache clusters), or trade exactness for throughput: each node keeps a local counter and syncs a delta to the central store every 50–100 ms, accepting a bounded over-admission equal to (N nodes × sync interval × local rate) instead of a network round-trip per request. Drill: memory for a rate limiter
L4 · ④ Time/Lifecycle — "two regions enforcing the same per-minute fixed window have clocks 300ms apart. What breaks?"
Trap: "NTP-sync the clocks tighter and the fixed window is fine."
Bar: clock skew is irrelevant to the real bug — a fixed window resets its counter at a wall-clock boundary regardless of skew, so a client firing the limit at :59.9 and again at :00.1 gets 2× the intended rate in 200ms even with perfect clocks; skew only makes cross-region enforcement additionally disagree about which window a request even falls in. Fix both problems the same way: anchor window boundaries at the central store's clock (not each node's), and use a sliding-window log or token bucket, which have no reset seam to exploit. Multi-Region Rate Limiting
L5 · ⑤ Adversary/Edge — "an attacker fans a burst out across 10,000 rotating IPs and API keys to dodge your per-key limiter. Now?"
Trap: "tighten the per-key limit further and add more IP-based rules."
Bar: a purely per-key/per-IP limiter cannot see a distributed attack because each identity individually looks under-limit — the fix is a second, coarser limiter keyed on a shared resource (a hot endpoint, a tenant, a shard) that catches aggregate abuse the per-identity view misses, plus a progressive-cost response (CAPTCHA/challenge) instead of a flat 429 once anomaly signals fire. This is the same asymmetry as hot-key skew: the attacker chooses the key distribution, you don't, so the defense has to key on something the attacker doesn't control. Rate-Limit Hot Keys
The floor keeps dropping: at L6+ they stack failure onto scale — "the shard holding the hot tenant's counter is also the one failing over during the botnet burst; walk me through what each request sees, in order" — there is no clean answer, only a graceful-degradation story with numbers attached.
Self-locate: died at L1 → mid-level; L4+ → staff signal.
Facing any new concept? Hit it with the six: concurrent? failing? at 100×? over time? adversarial? worth the cost? — that's the interviewer's whole playbook.
Re-authored and deepened for this guide. Sources: the token-bucket and leaky-bucket algorithms as documented in the AWS API Gateway usage-plan / throttle-and-burst model and NGINX/Envoy rate-limiting docs; the fixed-window boundary-burst problem and sliding-window counter as described in Alex Xu, System Design Interview (Ch. 4, "Design a Rate Limiter"); GitHub REST API rate-limit documentation (60 req/hr unauthenticated); Microsoft Graph throttling guidance (429/503 with Retry-After); and DesignGurus "What is rate limiting?". Worked trace values are illustrative and hand-verified.
🤖 Don't fully get this? Learn it with Claude
Stuck on What Is the Difference between Rate Limiting and Throttling and Quotas? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **What Is the Difference between Rate Limiting and Throttling and Quotas** (System Design) and want to truly understand it. Explain What Is the Difference between Rate Limiting and Throttling and Quotas from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **What Is the Difference between Rate Limiting and Throttling and Quotas** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **What Is the Difference between Rate Limiting and Throttling and Quotas** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **What Is the Difference between Rate Limiting and Throttling and Quotas** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.