In this post11 sections
- What the question asks and what it is testing
- The token bucket in one paragraph
- A short Python solution with tests
- Follow-up: make it thread safe
- Follow-up: one bucket per client without leaking memory
- Follow-up: share the limit across processes
- Token bucket or sliding window: how to choose out loud
- Is this really an FDE interview question?
- Questions people ask
- Keep reading
- More from the blog
“Implement a rate limiter” sounds like the easy question on the list. You write a class that says no when there are too many requests, then the interviewer asks about threads, then every customer’s API key, then several servers, and the warm-up has become the whole round. This post gives a short, tested Python , then each follow-up with the words to say. For where the coding round sits in the loop, read the FDE interview guide.
The short answer first. A token bucket stores two numbers per client: how many tokens it holds and when it last updated. On each request, add the tokens that dripped in since then (elapsed * rate), cap the total at capacity, and spend one if there is one. No timers and no background thread. The rest is follow-ups.
What the question asks and what it is testing
The prompt usually arrives in one line: “Write an allow() method that says whether a request may go through. Limit each user to some number of requests per second, with bursts.” The code fits on one screen. What gets judged is everything around it:
- Do you pin the contract before typing? Limit per what: user, API key, IP? Is a refused request rejected or delayed? Do some requests cost more than one token?
- Do you handle time correctly? Which clock, and how do you test refill without calling
sleep? - Do you find the edge cases yourself? Palantir’s coding guide tells candidates to think about edge cases and the ways their code could break. Source 1Writing Good CodePublisherPalantirSource typecompany hiring page
- Can you extend the design when the interviewer changes the problem? This is where the round is decided.
Here are the exact words we suggest for the opening, before any code:
“So
allow(client)returns true or false. Each client can burst up tocapacityrequests, then getsrateper second after that. A refused request spends nothing. I’ll inject the clock so I can test refill without sleeping. Should a refused caller be told when to retry?”
If the answer to that last question is yes, you will need the wait time for the HTTP response, and the solution below computes it.
Is this really an question? The public evidence is thin; we weigh it near the end. Our post on what no one publishes about FDE interviews shows how to check claims like it.
The token bucket in one paragraph
Picture a bucket that holds at most capacity tokens. Tokens drip in at rate per second. Each request takes one token, or cost tokens if requests differ in weight. When the bucket is empty, the request is refused or waits. Capacity sets the burst you allow; rate sets the long-run limit. The trick that keeps it cheap is lazy refill: nothing adds tokens on a schedule. On each call you work out how many would have dripped in since the last one. That gives constant time per call and two numbers per client. The token bucket glossary entry has the same idea in formula form.
Walk one example out loud before you code. With capacity=3 and rate=1:
- at
t=0, three requests pass and the fourth is refused; - at
t=0.5, the bucket holds half a token, so the next request is refused; - at
t=1.0, the halves add up to one token, so it passes; - after an hour idle, the bucket holds 3 tokens, not 3600.
The last line is the bug most first drafts have. Say it before the interviewer asks.
A short Python solution with tests
Standard library only. The clock is a parameter so tests can move time by hand.
import math
import time
class TokenBucket:
def __init__(self, capacity, rate, clock=time.monotonic):
if capacity <= 0 or rate <= 0:
raise ValueError("capacity and rate must be > 0")
# the largest burst
self.capacity = capacity
# tokens added per second
self.rate = rate
self.clock = clock
# start full
self.tokens = capacity
self.last = clock()
def allow(self, cost=1):
if not 0 < cost <= self.capacity:
raise ValueError("cost must be in (0, capacity]")
now = self.clock()
elapsed = max(0.0, now - self.last)
self.tokens = min(self.capacity,
self.tokens + elapsed * self.rate)
self.last = max(self.last, now)
# float refill can land just short
if self.tokens + 1e-9 >= cost:
self.tokens = max(0.0, self.tokens - cost)
return True
return False
def retry_after(self, cost=1):
# call right after a refused allow()
if self.tokens + 1e-9 >= cost:
return 0
return math.ceil((cost - self.tokens) / self.rate)
Say why each line is there as you write it:
time.monotonic, nottime.time. The wall clock can jump backwards when the host syncs its time, and a jump forward would mint free tokens. A monotonic clock only moves forward.max(...)on both the elapsed time andlastmeans a clock that steps backwards, such as a badly written fake, can neither remove tokens nor refill the same second twice.- A cost outside
(0, capacity]raises. A cost above capacity can never pass, so refusing it forever and telling the caller to retry would be a lie. A zero or negative cost would mint tokens: without the check,allow(-5)on a full bucket withcapacity=3returns true and leavestokens=8. 1e-9absorbs float error. Withrate=10and the clock stepping by 0.1, one step can refill 0.9999999999999998 tokens instead of 1. Without the tolerance, the float-steps test below fails; we checked.retry_afteris the wait a refused caller needs: the missing tokens divided by the rate. It rounds up, because the HTTPRetry-Afterheader takes whole seconds (more on that below), so a fast limit still says 1, and a client that obeys it is not refused again.
Now the tests. A small callable object stands in for the clock:
class FakeClock:
def __init__(self):
self.now = 0.0
def __call__(self):
return self.now
def test_burst_then_refill():
clock = FakeClock()
b = TokenBucket(capacity=3, rate=1, clock=clock)
got = [b.allow() for _ in range(4)]
assert got == [True, True, True, False]
clock.now = 0.5
# half a token is not enough
assert not b.allow()
clock.now = 1.0
# the two halves add up
assert b.allow()
def test_idle_never_overfills():
clock = FakeClock()
b = TokenBucket(capacity=3, rate=1, clock=clock)
clock.now = 1000.0
assert sum(b.allow() for _ in range(10)) == 3
def test_float_steps():
clock = FakeClock()
b = TokenBucket(capacity=1, rate=10, clock=clock)
for step in range(1, 51):
clock.now = round(step * 0.1, 10)
assert b.allow() and not b.allow()
def test_retry_after_rounds_up():
clock = FakeClock()
b = TokenBucket(capacity=3, rate=2, clock=clock)
assert all(b.allow() for _ in range(3))
clock.now = 0.2
# 0.4 tokens: 0.3 s to wait, sent as 1
assert not b.allow() and b.retry_after() == 1
Nothing sleeps, so say so: “I can test refill without a single sleep, so these tests will never be flaky in CI.”
If the limiter guards an HTTP API, a refused request gets status 429 Too Many Requests, defined in RFC 6585, section 4, which says the response may include a Retry-After header. RFC 9110, section 10.2.3 defines that header as an HTTP date or a whole number of seconds. So a refused request gets status 429 with Retry-After set to b.retry_after(), which the test above pins: 0.4 tokens left, 0.3 seconds to wait, sent as 1.
Follow-up: make it thread safe
The interviewer asks: “Your server handles requests on a thread pool. Is this safe?”
It is not. allow() reads the token count, decides, then writes it back. Two threads can both read the last token, both decide yes, and both spend it. Python’s global interpreter lock does not save you, because it can switch threads between the check and the subtraction. And on the free-threaded builds of Python 3.13 and later there is no global lock at all. The fix is to make refill, check and spend one step:
import threading
class LockedBucket(TokenBucket):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._lock = threading.Lock()
def allow(self, cost=1):
# refill, check and spend as one step
with self._lock:
return super().allow(cost)
The test is where you can stand out. A naive thread test proves very little: in our runs, the bucket without a lock passed it every time at Python’s default thread switch interval. Shorten the interval and the race shows:
import sys
def test_threads_never_overspend():
old = sys.getswitchinterval()
# switch threads often so races show
sys.setswitchinterval(1e-6)
try:
b = LockedBucket(capacity=100, rate=1e-9,
clock=lambda: 0.0)
wins = []
def work():
wins.append(sum(b.allow() for _ in range(1000)))
workers = [threading.Thread(target=work)
for _ in range(8)]
for w in workers:
w.start()
for w in workers:
w.join()
assert sum(wins) == 100
finally:
sys.setswitchinterval(old)
With the short interval, the bucket without a lock failed this test on some runs and passed on others, and the locked one passed every run. Say that out loud: “A passing concurrency test is weak evidence, so I made the race likely and then showed the lock closes it.”
One Blind commenter, who did not state their role, wrote in September 2024 that in their opinion Anthropic’s coding questions end with verbal follow-ups on scaling and edge cases. Source 2Anthropic interview advice (Blind)PublisherBlindSource typecandidate report on Blind That is one opinion, not about rate limiters or FDE roles specifically.
What about asyncio? If the server is a single asyncio event loop and allow() has no await inside it, no other task can run in the middle, so it needs no lock. The moment the bucket moves to a network store, that changes.
Follow-up: one bucket per client without leaking memory
Next: “Now every API key gets its own limit.” A dictionary of buckets keyed by client does it, and it leaks: every key ever seen stays in memory.
The insight that fixes it cleanly: a bucket left idle for capacity / rate seconds is full, and a full bucket behaves exactly like a brand-new one. So you can delete it without changing any answer the limiter gives. Keep the buckets in least-recently-used order and you only ever need to look at the oldest one:
from collections import OrderedDict
class PerClientLimiter:
def __init__(self, capacity, rate, clock=time.monotonic):
self.capacity = capacity
self.rate = rate
self.clock = clock
# idle this long means full
self.full_after = capacity / rate
# least recently used first
self.buckets = OrderedDict()
self.lock = threading.Lock()
def allow(self, client, cost=1):
# reject bad input before touching the dict
if not 0 < cost <= self.capacity:
raise ValueError("cost must be in (0, capacity]")
with self.lock:
now = self.clock()
# drop buckets that are full anyway
while self.buckets:
oldest = next(iter(self.buckets.values()))
if now - oldest.last < self.full_after:
break
self.buckets.popitem(last=False)
b = self.buckets.pop(client, None)
if b is None:
b = TokenBucket(self.capacity, self.rate,
self.clock)
# move to the newest end
self.buckets[client] = b
return b.allow(cost)
And its test:
def test_clients_are_independent_and_idle_ones_go():
clock = FakeClock()
lim = PerClientLimiter(capacity=2, rate=1, clock=clock)
assert lim.allow("a") and lim.allow("a")
assert not lim.allow("a")
# a's empty bucket does not affect b
assert lim.allow("b")
# both idle past capacity / rate
clock.now = 5.0
assert lim.allow("c")
assert list(lim.buckets) == ["c"]
The oldest bucket always has the oldest last, because every call moves its client to the newest end and the clock only moves forward. That is also why the cost check comes first: a call that raised after moving a bucket would break the order. Each bucket is evicted at most once, so the cleanup costs constant time per call on average.
Then name the case this does not cover. Memory is now bounded by the clients active in the last capacity / rate seconds. If clients are keyed by IP address and someone sprays requests from many addresses, that is still unbounded, so add a hard cap on the number of buckets and decide what happens past it: refuse new clients, or put them in one shared bucket.
Follow-up: share the limit across processes
Next: “We run several app servers behind a load balancer.” Now each process has its own dictionary, and a client gets the full limit once per process.
You have three options. Say them in order of cost:
- Split the limit. Give each process
capacity / Nandrate / N. No shared state and no new failure mode, but it is only right when the load balancer spreads each client’s requests evenly. With sticky sessions, a client gets one process’s share. - Share the state, naively. Store each bucket in Redis, then read it, compute and write it back. This brings back the thread race, across machines: two servers read the last token and both spend it.
- Share the state, atomically. Move the arithmetic into the store so refill, check and spend run as one step. In Redis that is a Lua script, and Redis runs a script atomically. Two details make it correct. The script reads Redis’s own
TIME, so servers with skewed clocks agree. And it sets a key expiry ofcapacity / rateseconds, the same “idle means full” argument as the eviction above. The full script is in the model answer to the token bucket question.
Then the question that separates candidates: “What happens when Redis is slow or down?” There is no free answer, so give the options and ask:
- Fail open. Allow everything. Right when the limiter protects your own servers and a short overload is survivable.
- Fail closed. Refuse everything. Right when the limit protects a partner’s metered quota or something that costs money.
- Fall back. Each process switches to a local bucket with its share of the limit. It is approximate, but it degrades gently.
Whichever you pick, give the Redis client a short timeout, or every request waits out a long one while the store is down. Here is what to say:
“If Redis is down, I’d fall back to a local bucket with each server’s share of the limit, and alert on it. If this limit protects the partner’s quota, I’d fail closed instead. Which failure hurts the customer less?”
That last question is the FDE part: the trade-off belongs to the customer.
Token bucket or sliding window: how to choose out loud
Expect: “Why not a sliding window?” Know both well enough to compare them without notes.
| Token bucket | Sliding-window log | |
|---|---|---|
| Bursts | Up to capacity at once | Never over the limit in any window |
| State per client | Two numbers | A timestamp per recent request |
| Work per call | Constant | Drop expired timestamps first |
| Easy to state | “rate per second, bursts of capacity” | “At most limit in any window” |
A third option sits between them. A sliding-window counter keeps two fixed-window counts per client and weights the previous one by how much of it still overlaps the window, so it approximates the log with constant state. And name the one to avoid: a plain fixed-window counter lets a burst at the end of one window and another at the start of the next both pass. The sliding-window question has the log in code and the memory arithmetic.
How to choose out loud:
“If the contract says ‘at most this many per minute’ and a partner will audit it, I’d use a sliding log, because it matches the wording exactly. If the goal is to protect our servers and short bursts are fine, a token bucket is cheaper and friendlier to clients. Which one describes your limit?”
Finally, flip sides. In FDE work you may be the client being limited, not the server doing the limiting. A client that gets HTTP 429 should wait for Retry-After when it is present and back off with when it is not, or every client retries at the same moment. The retry jitter question covers that code, and our post on the API integration coding round puts it together with pagination and idempotency.
Is this really an FDE interview question?
You will see the claim repeated. One prep site lists “Implement a rate limiter (Anthropic favorite)” among common FDE coding patterns and cites no source. Source 3Forward Deployed Engineer Interview: The Definitive 2026 GuidePublisherExponent (Aced)Source typeinterview prep siteSource 4Anthropic Technical Interview Questions: Complete Guide 2026PublisherJobrightSource typeinterview prep site Another prep site lists “Design a rate limiter” as an Anthropic concurrency question and links one candidate’s Medium post. Source 3Forward Deployed Engineer Interview: The Definitive 2026 GuidePublisherExponent (Aced)Source typeinterview prep siteSource 4Anthropic Technical Interview Questions: Complete Guide 2026PublisherJobrightSource typeinterview prep site Neither shows what Anthropic asks.
What candidates and public repos show:
- A design round. Two posters on Reddit wrote, between April and June 2026, about Heizen’s Forward Deployed Engineer role and a first round on low-level or API design; one said it was designing a tier-based API system with a rate limiter, in pseudocode. Source 5Heizen Forward Deployed Engineer Interview – What to Expect in Next Rounds? (post by u/ByteTrooper)PublisherReddit r/developersIndiaSource typecandidate report on RedditSource 6Heizen Forward Deployed Engineer Interview – What to Expect in Next Rounds? (comment by u/Negative_Host_4149)PublisherReddit r/developersIndiaSource typecandidate report on RedditSource 7Heizen Forward Deployed Engineer Interview – What to Expect in Next Rounds? (comment by u/Electrical_Abies_464)PublisherReddit r/developersIndiaSource typecandidate report on RedditSource 8Heizen Forward Deployed Engineer Interview – What to Expect in Next Rounds? (comment by u/Electrical_Abies_464)PublisherReddit r/developersIndiaSource typecandidate report on RedditSource 9Heizen Forward Deployed Engineer Interview – What to Expect in Next Rounds? (comment by u/Electrical_Abies_464)PublisherReddit r/developersIndiaSource typecandidate report on Reddit
- A take-home. One candidate’s GitHub repo describes the Quilr AI take-home for a Solutions Engineer / Forward Deployed Engineer role as four control-plane tasks, one of them token rate limiting with model failover. Source 10Quilr FDE take-homePublisherPalmCoast (GitHub)Source typecandidate’s take-home repository
- A deliberately rate-limited API. Forerunner Industries has a repo named fde-take-home with a mock permit-system API built with intentional issues, including rate limiting after 5 consecutive requests within 10 seconds. Source 11Permit System APIPublisherForerunner Industries (GitHub organization)Source typecompany website
These are single reports and repos, not a pattern. The reason to practice anyway is the job: FDE code talks to customer and partner APIs you do not control, and those can refuse you with HTTP 429, as the Forerunner mock API does. Our lesson on what FDE coding rounds test sorts the evidence by company, and do FDE interviews have LeetCode? covers the split between practical builds and algorithm problems.
Before your coding round
- Write the bucket from memory with an injected clock, and time yourself.
- Test burst, partial refill, the cap after a long idle, float steps and the rounded-up
Retry-After. - Explain the race in one sentence, then close it with a lock.
- Explain why an idle bucket can be deleted without changing any answer.
- Describe the atomic shared version and pick fail-open, fail-closed or fall back, with a reason.
- Compare the token bucket with a sliding window, and end with a question for the customer.
Set a 25-minute timer, write the bucket from memory with an injected clock, then open the token bucket question and answer its follow-ups out loud before you read the model answer, which has the full Redis script and the fallback when Redis is down. Our question bank holds 180 interview questions, and the token bucket, sliding-window and retry jitter questions are free to read.
Questions people ask
How does a token bucket rate limiter work?
A bucket holds up to a fixed number of tokens and refills at a steady rate. Each request takes a token, and when none is left the request is rejected or delayed. The capacity sets the burst you allow, and the refill rate sets the long-run limit.
Should I implement a token bucket or a sliding window?
A token bucket allows short bursts and keeps constant state per client. A sliding-window log counts requests in the last window exactly but stores a timestamp for every request. Ask which behavior the customer needs, then say why you chose.
Is the rate limiter an Anthropic interview question, as prep sites say?
One prep site says it is an Anthropic favorite and cites no source, and another prep site lists it as an Anthropic concurrency question, linking one candidate’s blog post. Treat both as unverified, and practice it anyway, because the APIs an FDE integrates with can rate-limit you.Source 3Forward Deployed Engineer Interview: The Definitive 2026 GuidePublisherExponent (Aced)Source typeinterview prep siteSource 4Anthropic Technical Interview Questions: Complete Guide 2026PublisherJobrightSource typeinterview prep site
How do I make a rate limiter work across several servers?
Keep the bucket state in a shared store and update it atomically, for example with a Redis script that refills and takes a token in one step. Then say what happens when the store is slow or down, because failing open or failing closed is a decision for the customer.
Keep reading
Questions
- Implement a per-user token bucket rate limiter. Then make it work across several processes.
- Implement a sliding-window rate limiter and explain its memory cost.
- Write a retry wrapper with exponential backoff and full jitter. Which errors should it not retry?
- A downstream service recovers from an outage and immediately falls over again. Why, and how do you prevent it?
- Implement a circuit breaker with half-open probing.
More from the blog
Interview rounds
Parse a messy CSV in a coding interview: normalize, report what you dropped, and say why
Parse a messy CSV in a coding interview: detect encoding and delimiter, normalize dates and money, keep a rejects report, and make reruns safe.
Interview rounds
The API integration coding round: retries, pagination and idempotency without the panic
The FDE API integration round, worked in Python: backoff with jitter, cursor pagination that resumes, idempotency keys, and the retry mistake to avoid.
Interview rounds
AI-assisted coding interviews: how to use the assistant so they see your judgment
When the interview allows an AI assistant, your judgment is what gets seen. Published rules, what candidates report, and the words to narrate each step.