Knowledge Guide
HomeSystem DesignSystem Design Problems

hard Rate Limiting Algorithms: Token Bucket vs Leaky Bucket

Two knobs, one job: cap the request rate without punishing legitimate traffic

A token bucket refills at a steady rate and lets a request through only if it can debit a token, so idle time banks permits that can be spent all at once — a burst passes at full speed until the bank is empty, then throughput settles to the refill rate. A leaky bucket is instead a fixed-depth queue draining at a steady rate: requests queue up to be admitted, but they leave — and hit whatever sits downstream — at a constant pace no matter how bursty the arrivals were. Same average rate, opposite behavior at the burst: one is a limiter that tolerates spikes, the other is a shaper that erases them.

The two update rules

Token bucket (capacity C, refill rate r/s). Store two numbers — tokens and last_updated — and refill lazily on each request rather than running a background timer:

tokens = min(C, tokens + (now - last_updated) * r)   // lazy refill, capped at C
last_updated = now
if tokens >= 1: tokens -= 1; admit
else: reject (429)

Leaky bucket (queue depth C, drain rate r/s). Store queue_len and last_drained, and leak lazily the same way:

queue_len = max(0, queue_len - (now - last_drained) * r)   // lazy leak
last_drained = now
if queue_len < C: queue_len += 1; admit (queued, released downstream at rate r)
else: reject (429, overflow)
A burst of 20 requests at t=0 against capacity 10, rate 5/s: the token bucket admits 10 instantly then refills one retry every 0.2s; the leaky bucket enqueues 10 and drains them one every 0.2s so downstream never sees more than one at a time
A burst of 20 requests at t=0 against capacity 10, rate 5/s: the token bucket admits 10 instantly then refills one retry every 0.2s; the leaky bucket enqueues 10 and drains them one every 0.2s so downstream never sees more than one at a time

Traced: a burst of 20 hits capacity 10, rate 5/s

Both buckets start ready (token bucket full at 10 tokens, leaky bucket's queue empty) and see all 20 requests arrive simultaneously at t=0.

Requests 1–10 (t=0)Requests 11–20 (t=0)After t=0
Token bucketadmitted instantly — 10 tokens spent, bucket → 0rejected — bucket empty, 0 tokens (429)+1 token every 0.2s (1⁄r) → 1 retried request admitted every 0.2s; the 10th retry is admitted at t=2.0s
Leaky bucketenqueued — queue fills to 10/10rejected — queue full, overflow (429)1 request leaves the queue every 0.2s → a constant 5/s hits downstream; the 10th leaves at t=2.0s

Read the difference carefully: both admit exactly 10 immediately and reject the same 10, and both fully drain the accepted 10 by t=2.0s (10 requests ÷ 5/s = 2.0s). Where they diverge is what downstream sees. The token bucket's 10 admitted requests are handed off now — downstream takes an instantaneous spike of 10 at t=0. The leaky bucket's 10 admitted requests are queued, then released one every 0.2s — downstream never sees more than 1 at a time. Token bucket is a limiter that tolerates bursts; leaky bucket is a shaper that erases them and trades the spike for up to 2.0s of queueing latency on the last admitted request.

Fixed window: cheapest, but the boundary lets 2× through

A fixed window keeps one counter per clock-aligned window (e.g. per second) and resets it when the window rolls over; admit while count < limit. It's a single INCR + TTL — the cheapest limiter to run, and trivially shared across servers in Redis. Its flaw is the reset itself: nothing tracks how close together requests near a boundary actually are.

Traced: limit = 5 per 1-second window, matching the 5/s target rate. Window A = [0s,1s): 5 requests land at t=0.80–0.99s — count 5 ≤ limit 5, all admitted. Window B = [1s,2s): 5 more requests land at t=1.00–1.08s — count 5 ≤ limit 5, all admitted. Both windows are individually legal. But look at the wall clock: 10 requests landed inside 1.08−0.80 = 0.28 seconds — an instantaneous rate of 10⁄0.28 ≈ 36 req/s, over 7× the intended 5/s, with 2× the per-window limit crossing the boundary in a moment.

Sliding window fixes it — at a memory cost

A sliding-window log stores every request's timestamp and counts how many fall in the trailing window seconds — exact, but memory grows with request volume per key. Evaluate it the instant window B's first request arrives, at t=1.00s: the trailing 1-second window (0.00s,1.00s] already contains window A's 5 requests plus this new one — 6 > limit 5 — so it is rejected immediately, closing the exact bug above (a fixed window would have let it through because it resets, rather than slides).

A sliding-window counter gets most of that accuracy back at O(1) memory: it blends the current window's count with a time-weighted fraction of the previous window's count.

estimate = current_count + previous_count * (1 - elapsed_into_current / window)

At that same instant, t=1.00s (0% into window B): estimate = 1 + 5 × (1 − 0) = 6, above the limit of 5 — it throttles too, without storing a single timestamp. It's an approximation (a lopsided traffic shape inside the previous window makes the weighting inexact), but in practice it tracks the log closely enough that it is the usual production choice (this is the approach Cloudflare documents for its own rate limiter).

A fixed 1-second window with limit 5: 5 requests near the end of window A and 5 just after the start of window B both pass their own window's check, but land within a 0.28 second span, roughly 36 requests per second, twice the intended rate. A sliding-window log or weighted counter correctly rejects starting from the first request of window B
A fixed 1-second window with limit 5: 5 requests near the end of window A and 5 just after the start of window B both pass their own window's check, but land within a 0.28 second span, roughly 36 requests per second, twice the intended rate. A sliding-window log or weighted counter correctly rejects starting from the first request of window B

Pitfalls

Choosing among the four

The real interview question is rarely "token bucket or leaky bucket" in isolation — it's which of these four fits the endpoint in front of you.

One-line rule: let the burst through (token bucket) when downstream can take it and clients legitimately spike; smooth the burst away (leaky bucket) when downstream can't; use a fixed window only for cheap, coarse quotas; reach for a sliding-window counter whenever you need real accuracy without a full log.

Two things get harder once this single-node picture goes distributed, each covered on its own page: a single hot tenant key still bottlenecks one Redis shard no matter how the cluster is sized, because sharding spreads different keys, never one key (see Rate-Limit Hot Keys); and once the limiter runs in multiple regions, the shared quota becomes a consistency problem where PACELC decides whether you enforce a strict, slower global count or a fast, locally-approximate one (see Multi-Region Rate Limiting).

Takeaways


Mechanisms follow Tanenbaum & Wetherall's traffic-shaping treatment of token/leaky buckets and the ATM Forum's GCRA (virtual scheduling) formulation of the leaky bucket; the sliding-window counter follows Cloudflare's published rate-limiting design; production usage per Stripe, GitHub, AWS, and NGINX limit_req engineering docs. Re-authored and deepened for this guide with a from-scratch numeric trace (C=10, r=5/s, burst of 20) and a derived fixed/sliding-window boundary-bug example. See also: Rate-Limit Hot Keys; Multi-Region Rate Limiting.

🤖 Don't fully get this? Learn it with Claude

Stuck on Rate Limiting Algorithms: Token Bucket vs Leaky Bucket? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Rate Limiting Algorithms: Token Bucket vs Leaky Bucket** (System Design) and want to truly understand it. Explain Rate Limiting Algorithms: Token Bucket vs Leaky Bucket 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.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Rate Limiting Algorithms: Token Bucket vs Leaky Bucket** 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.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Rate Limiting Algorithms: Token Bucket vs Leaky Bucket** 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.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Rate Limiting Algorithms: Token Bucket vs Leaky Bucket** 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.

📝 My notes