Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Token Bucket Rate Limiter
The algorithm inside most production rate limiters — and it never runs a timer. A bucket refills lazily: on each request, credit the time elapsed since the last one, cap at capacity, spend one token or reject. Two numbers of state per client.
The problem
Implement a token bucket as a pure replay: given capacity (max tokens), rate (tokens refilled per second), and times — a non-decreasing list of request timestamps in seconds — return a list of booleans, one per request: accepted or rejected. The bucket starts full. Refill lazily: at each request, add (t − t_last) · rate tokens (capped at capacity, where t_last is the previous request time), then accept if at least 1 token remains (spend it) or reject (spend nothing).
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement token_bucket(capacity, rate, times) → list of accept/reject booleans, using lazy fractional refill from a bucket that starts full.
Examples and constraints 4 examples
capacity = 2, rate = 1, times = [0, 0, 0, 1, 3][true, true, false, true, true]capacity = 5, rate = 1, times = [0, 1, 2, 3, 4][true, true, true, true, true]capacity = 1, rate = 0.5, times = [0, 1, 2][true, false, true]capacity = 2, rate = 10, times = [0, 100, 100, 100][true, true, true, false]- The bucket starts full (tokens = capacity).
- Refill is lazy and fractional — no timers, no rounding.
- Rejected requests spend nothing (tokens unchanged apart from the refill).
- times is non-decreasing; equal timestamps are legal (a burst).
Hints 4 ideas
- Keep two state variables: current tokens and the previous request timestamp.
- On each request: tokens = min(capacity, tokens + (t − t_last) · rate), then t_last = t.
- Accept iff tokens ≥ 1; subtract 1 only on accept.
- This is exactly what production limiters store per key in Redis — two numbers, no timer.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious approach is what the word “refill” suggests: a timer that adds rate · dt tokens every dt seconds, simulated in a replay by stepping a clock tick by tick up to each request. It works when every timestamp lands on a tick. It costs O(n + T/dt) time for n requests spanning T seconds, and O(1) state. It is too slow and also wrong: times = [0, 1000000] with 1-second ticks runs a million ticks for two requests, and with capacity = 1, rate = 1, times = [0, 1.5, 2.2] it accepts all three, because the tick at t = 2 credits a whole token where only 0.7 seconds have passed since the last request.
The approach
Nothing spends tokens between two requests, so the level at a request is a closed-form function of the level at the previous one: add the elapsed time times the rate, then cap at capacity. Refilling and capping continuously would give exactly the same number, which is why the lazy formula needs no timer. The invariant: after each request, tokens is the exact level a continuously refilling bucket would hold at time last.
The reference keeps that state in two variables, tokens = float(capacity) (the bucket starts full) and last = times[0] if times else 0. For each t it computes tokens = min(capacity, tokens + (t - last) * rate), then sets last = t whether or not the request passes, because the refill already happened. If tokens >= 1 it subtracts one and appends True to out; otherwise it appends False and leaves tokens alone, so a rejection spends nothing.
Walk through an example
Tracing the first example, capacity = 2, rate = 1, times = [0, 0, 0, 1, 3], one request per row.
| step | t | refill (t − last) · rate | tokens after refill | decision | tokens after |
|---|---|---|---|---|---|
| 1 | 0 | 0 | 2 | accept | 1 |
| 2 | 0 | 0 | 1 | accept | 0 |
| 3 | 0 | 0 | 0 | reject, spends nothing | 0 |
| 4 | 1 | 1 | 1 | accept | 0 |
| 5 | 3 | 2 | min(2, 0 + 2) = 2 | accept | 1 |
| return | — | — | — | [True, True, False, True, True] | — |
Complexity
Lazy refill: O(n) time for n requests, one multiplication, one min and one comparison each, and O(1) state (two numbers) besides the O(n) output list. Tick simulation: O(n + T/dt) time for a span of T seconds with tick dt, because it also pays for every quiet tick, and the same O(1) state.
Edge cases to test
- Empty
times: return[]. The reference guardstimes[0]withif times else 0; reading the first timestamp unguarded crashes. - A long idle gap,
capacity = 2, rate = 10, times = [0, 100, 100, 100]:[true, true, true, false]. The cap holds the bucket at 2 tokens; without it, 100 idle seconds bank 1,000 tokens and all four pass. rate = 0withcapacity = 1, times = [0, 0, 0]:[true, false, false]. The bucket never refills, and rejections leave it at 0, not negative.- Equal timestamps are a burst: three requests at t = 0 against
capacity = 2give[true, true, false], because zero elapsed time refills nothing. - A capacity below one token, e.g.
capacity = 0.5, rate = 1, times = [0, 1, 2]:[false, false, false]. The level can never reach a whole token, so nothing is ever accepted. - A rejection still moves
last:capacity = 2, rate = 0.5, times = [0, 0, 1, 1.5]must give[true, true, false, false]. Updatinglastonly on accept credits the interval from 0 to 1 a second time at t = 1.5 and wrongly accepts that request.
Common mistakes
- Refilling with a background loop instead of lazily on access — needless work and clock coupling.
- Not capping accrued tokens at capacity, which lets a long-idle client burst past the limit.
- Floating-point drift when accumulating fractional tokens: ten refills of 0.1 add up to
0.9999999999999999, not 1, so atokens >= 1check rejects a request that exact arithmetic would accept. - Updating
lastonly when a request is accepted. The refill for a rejected request already happened, so the next request is credited for the same interval twice.
Follow-ups an interviewer asks next
- Many API servers enforce one shared limit per client.
- Keep
(tokens, last)per key in a shared store such as Redis and run refill, check and spend as one atomic step, for example a Lua script executed by the server. Two servers doing a plain read, modify and write can both read 1 token and both accept. Take timestamps from one clock, such as the store’s, so skew between servers cannot mint extra tokens. - Requests have different costs, such as tokens per LLM call.
- Spend the cost instead of 1: accept when
tokens >= costand subtractcost. Then decide what happens to a request that costs more thancapacity, because it can never pass: reject it with a clear error, or admit it by letting the level go negative and repaying the debt from future refills. - Fractional rates drift. How do you make the arithmetic exact?
- With
capacity = 1, rate = 0.1and one request per second, ten refills of 0.1 sum to0.9999999999999999, so this reference rejects the request at t = 10 that exact arithmetic accepts. Store the level in integer units chosen so that rate times elapsed time is always whole (tenths of a token here), or use Python’sfractions.Fraction. An epsilon in the comparison also works but hides the cause. - Tell a rejected client when to retry.
- The level is short by
1 - tokensand grows atrateper second, so if nothing else spends from the bucket first, a retry can pass after(1 - tokens) / rateseconds (never, if the rate is 0). Send it rounded up in aRetry-Afterheader so well-behaved clients wait instead of hammering the limiter.
Where this shows up
Token bucket is the standard rate-limiting algorithm: capacity sets the allowed burst, refill rate sets the sustained limit. It is used in API gateways, network traffic shaping, and anywhere you need to smooth bursty load while still permitting short spikes.
Reference solution
Reference solution — Python
def token_bucket(capacity, rate, times):
tokens = float(capacity)
last = times[0] if times else 0
out = []
for t in times:
tokens = min(capacity, tokens + (t - last) * rate)
last = t
if tokens >= 1:
tokens -= 1
out.append(True)
else:
out.append(False)
return out
print(token_bucket(2, 1, [0, 0, 0, 1, 3])) # -> [True, True, False, True, True]Reference solution — TypeScript
function tokenBucket(capacity: number, rate: number, times: number[]): boolean[] {
let tokens = capacity;
let last = times.length ? times[0] : 0;
const out: boolean[] = [];
for (const t of times) {
tokens = Math.min(capacity, tokens + (t - last) * rate);
last = t;
if (tokens >= 1) {
tokens -= 1;
out.push(true);
} else {
out.push(false);
}
}
return out;
}
console.log(tokenBucket(2, 1, [0, 0, 0, 1, 3])); // -> [true, true, false, true, true]Questions about Token Bucket Rate Limiter
What is the difference between a token bucket and a leaky bucket?
A token bucket stores permission: tokens accumulate up to capacity while a client is quiet, so a burst of up to capacity requests can pass at once, and after that the sustained rate applies. A leaky bucket used as a queue stores the requests themselves and releases them at a fixed rate, so it delays and smooths bursts instead of letting them through. Used as a meter rather than a queue, the leaky bucket is the mirror image of the token bucket and accepts the same requests. Choose a token bucket when short bursts are acceptable and you want to reject rather than delay.
Why does a token bucket refill lazily instead of on a timer?
Because nothing spends tokens between two requests, the level at the next request is fully determined by the elapsed time: add the elapsed seconds times the rate and cap at capacity. Computing that on arrival gives exactly the number a perfect timer would have produced, with no timer per client, no wake-ups for idle clients, and no rounding to timer ticks. The whole state is two numbers, the current level and the last timestamp, which is why the pattern scales to millions of keys in a shared store.
How do capacity and rate map to a limit like 100 requests per minute?
The rate sets the sustained limit: 100 requests per minute is a rate of 100 divided by 60, about 1.67 tokens per second. Capacity sets the largest burst a quiet client can spend at once. Over any stretch of w seconds a token bucket accepts at most capacity plus rate times w requests, because it can start that stretch full and refills only at the rate. So capacity 100 lets a full minute of quota go out in one burst, while capacity 10 allows bursts of 10 and then about one request every 0.6 seconds.
Is a token bucket better than a sliding window rate limiter?
They answer different questions. A token bucket keeps two numbers per client and deliberately allows bursts up to its capacity. A sliding window log keeps the timestamps of recently accepted requests, up to the limit per client, and guarantees that no window of the chosen length ever holds more than the limit. A sliding window counter approximates the log with two counters per client. Pick the token bucket when bursts are acceptable and memory matters, and the log when the per-window cap must be exact.
This idea in other formats
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- LRU CacheThe eviction policy behind every size-limited cache: when you run out of room, throw out whatever was used least recently. The trick is O(1) get and put — an ordered hash map (Python dict / JS Map) gives you exactly that. Solve it in Python or TypeScript.Read →
- Streaming MedianLatency dashboards do this every second: maintain the median of a stream without re-sorting per event. The classic two-heap trick — a max-heap for the low half, a min-heap for the high half, the median always at the boundary. Solve it in Python or TypeScript.Read →
- Circuit BreakerStop one failing dependency from taking down the fleet: trip after consecutive failures, fail fast while open, probe once after the cooldown. Implement the closed → open → half-open state machine as a pure, testable replay. Solve it in Python or TypeScript.Read →
- Sliding-Window Rate LimiterAllow at most N requests per rolling window — the rate limiter that guards real APIs. A sliding log of accepted timestamps gives exact limits without fixed-window bursts. Decide accept/reject for a stream. Solve it in Python or TypeScript, with hidden tests.Read →
- LFU CacheThe cache that evicts what you use least often — and, on ties, least recently. Harder than LRU: track frequency and recency together, still O(1) per op. Replay get/put operations. Solve it in Python or TypeScript, with hidden tests.Read →
- Consistent Hashing RingHow distributed caches decide which node owns a key — with tiny churn when nodes join or leave. Build a hash ring with virtual nodes and route keys clockwise. A provided hash keeps Python and TypeScript in sync. Hidden tests.Read →