InterviewPrepKit

Home / Coding / Agent Coding / Reliability & Retries / Token-Bucket Rate Limiter

Token-Bucket Rate Limiter

hard 00:00
Solving tips
  • Refill lazily: instead of a background timer, compute how many tokens accrued from elapsed time on each allow() call — this is exact and needs no thread.
  • Capacity B caps the burst (how many requests can fire back-to-back after idle); rate R caps the sustained throughput (tokens/sec over the long run).
  • Inject now() as a clock stub so tests advance time by hand and stay deterministic — never call real time.monotonic in gradeable code.

Build a token-bucket rate limiter — the workhorse for throttling calls to an LLM API, a downstream tool, or any endpoint with a quota. It is the mechanism that lets you absorb a short burst of traffic while still holding the long-run average to a fixed rate.

How a token bucket works

Picture a bucket that holds up to capacity tokens and is topped up continuously at refill_rate tokens per second, never overflowing past capacity. Every request must take cost tokens out to proceed. If the bucket has enough, the request is allowed and the tokens are removed; if not, the request is throttled and nothing is spent.

You do not need a background timer to add tokens. Refill lazily: on each allow() call, look at how much time has passed since the previous call and credit elapsed * refill_rate tokens (clamped to capacity). This is exact, thread-free, and driven entirely by the injected now() clock.

Burst vs sustained rate

Two numbers describe the limiter, and interviewers want you to separate them:

  • Sustained rate R (refill_rate) is the long-run ceiling. Over a long window you can make at most about R requests per second, because that is how fast tokens come back.
  • Burst B (capacity) is the slack. After an idle period the bucket is full, so up to B requests can fire back-to-back in an instant before the bucket empties and you fall back to the drip rate R.

So capacity=10, refill_rate=1 means “1 request/second sustained, but up to 10 in a sudden burst.” Raising capacity makes the limiter more forgiving of spikes; raising the rate raises the steady throughput.

Task

Implement the TokenBucket class:

  1. __init__(capacity, refill_rate, now) stores the parameters, starts the bucket full (tokens = capacity), and records the current time from now() as the last-refill timestamp. If now is None, default it to time.monotonic.
  2. allow(cost=1.0):
    • Read the current time t = now(), compute elapsed = t - last_time, add elapsed * refill_rate tokens capped at capacity, and set last_time = t.
    • If tokens >= cost, subtract cost and return True.
    • Otherwise return False without changing the token count.

Example

clock = [0.0]                      # mutable clock the test advances by hand
bucket = TokenBucket(capacity=2, refill_rate=1.0, now=lambda: clock[0])

bucket.allow()   # -> True   (tokens 2 -> 1)
bucket.allow()   # -> True   (tokens 1 -> 0)
bucket.allow()   # -> False  (empty, no time has passed)

clock[0] = 1.0                     # one second later: +1 token
bucket.allow()   # -> True   (refilled to 1, then 1 -> 0)

Constraints

  • Do not call time.sleep, spawn a thread, or use a background timer — refill is computed from elapsed time on each call.
  • Never let the token count exceed capacity after a refill, and never let it go below zero.
  • A throttled call (returns False) must not consume tokens; only the refill portion may change the count.
  • Read the clock exactly through the injected now() so tests are deterministic; default to time.monotonic when now is not supplied.

Write your solution, then hit Run tests to check it — or get a mock grade from the AI coach.

The coach remembers this session — revise your code and ask again, and it grades your progress. It gives hints, not the answer.
Report a bug