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 aboutRrequests 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 toBrequests can fire back-to-back in an instant before the bucket empties and you fall back to the drip rateR.
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:
__init__(capacity, refill_rate, now)stores the parameters, starts the bucket full (tokens = capacity), and records the current time fromnow()as the last-refill timestamp. IfnowisNone, default it totime.monotonic.allow(cost=1.0):- Read the current time
t = now(), computeelapsed = t - last_time, addelapsed * refill_ratetokens capped atcapacity, and setlast_time = t. - If
tokens >= cost, subtractcostand returnTrue. - Otherwise return
Falsewithout changing the token count.
- Read the current time
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
capacityafter 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 totime.monotonicwhennowis not supplied.