Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°22

Token Bucket Rate Limiter

Medium SystemsRate LimitingDistributed Systems

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).

SOLVE IT YOURSELF

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.

YOUR TASK

Implement token_bucket(capacity, rate, times) → list of accept/reject booleans, using lazy fractional refill from a bucket that starts full.

CPython · WebAssembly
Examples and constraints 4 examples
EXAMPLE 1
Input capacity = 2, rate = 1, times = [0, 0, 0, 1, 3]
Output [true, true, false, true, true]
burst of 2 passes, third rejected, refill revives later requests
EXAMPLE 2
Input capacity = 5, rate = 1, times = [0, 1, 2, 3, 4]
Output [true, true, true, true, true]
one request per second never outpaces one token per second
EXAMPLE 3
Input capacity = 1, rate = 0.5, times = [0, 1, 2]
Output [true, false, true]
fractional refill: 1 token back after 2 seconds
EXAMPLE 4
Input capacity = 2, rate = 10, times = [0, 100, 100, 100]
Output [true, true, true, false]
edge case: 100 idle seconds refill only up to the cap of 2 tokens
CONSTRAINTS
  • 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
HINTS — 4 IDEAS
  1. Keep two state variables: current tokens and the previous request timestamp.
  2. On each request: tokens = min(capacity, tokens + (t − t_last) · rate), then t_last = t.
  3. Accept iff tokens ≥ 1; subtract 1 only on accept.
  4. 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.

steptrefill (t − last) · ratetokens after refilldecisiontokens after
1002accept1
2001accept0
3000reject, spends nothing0
4111accept0
532min(2, 0 + 2) = 2accept1
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 guards times[0] with if 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 = 0 with capacity = 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 = 2 give [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]. Updating last only 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 a tokens >= 1 check rejects a request that exact arithmetic would accept.
  • Updating last only 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 >= cost and subtract cost. Then decide what happens to a request that costs more than capacity, 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.1 and one request per second, ten refills of 0.1 sum to 0.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’s fractions.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 - tokens and grows at rate per second, so if nothing else spends from the bucket first, a retry can pass after (1 - tokens) / rate seconds (never, if the rate is 0). Send it rounded up in a Retry-After header 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.

▶  Watch it explained
Finished this one? 0 / 76 Challenges done
NextSliding-Window Rate Limiter Next on the “Systems, limits & distribution” ladder · rung 4 of 9

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