The Throttle Wall
A hard Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- hard
- Seniority
- L4
- Asked in
Problem
An API gateway throttles each client independently: within any window_ms window a client may keep at most max_requests accepted calls. The operations list opens with ['init', max_requests, window_ms] to configure the limiter and is followed by ['is_allowed', client_id, timestamp] decisions; return one result per entry, None for the configure step and then True or False for each decision. A call is allowed only when the client has fewer than max_requests accepted calls still inside the half-open window (timestamp - window_ms, timestamp], and a denied call is never recorded, so a throttled client becomes eligible again as soon as its earlier accepted calls age out.
Stop the abusers. Let the rest through.
Example
Input:
operations = [["init",2,1000],["is_allowed","clientA",100],["is_allowed","clientA",200],["is_allowed","clientA",300],["is_allowed","clientA",1101],["is_allowed","clientB",300]]
Output:
[null,true,true,false,true,true]
Worked solution and explanation
What this really is
Strip the API-gateway costume and this is a per-client sliding window with two decisions that separate candidates. Anyone can keep a deque of accepted timestamps per client; the interview lives in two places. First, the eviction boundary: the window is half-open, so a call exactly window_ms old has already expired and must free a slot. Write the eviction with > instead of >= and a client calling exactly one window after its oldest accepted call is wrongly denied. Second, denials leave no trace: record clientA's rejected call at t=300 and its call at t=1101 comes back False, and a client that keeps retrying keeps pushing its own cooldown out forever. All of it has to live per client, or clientA's burst throttles clientB.
Break down the requirements
Step 1: Match the operations contract exactly
You drive a list of operations. The first entry ['init', max_requests, window_ms] configures the limiter (milliseconds, integers) and contributes None. Every later ['is_allowed', client_id, timestamp] op contributes True or False. The function returns one result per op, in order, so the results list lines up index-for-index with operations.
Step 2: Isolate state per client
A defaultdict(deque) keyed by client_id gives each client an independent timestamp queue without an explicit existence check. Sharing a single deque across clients lets one chatty client throttle everyone else, which the visible test catches: clientB at t=300 is allowed because its own deque is empty even though clientA is currently throttled.
Step 3: Evict in milliseconds, deny without recording
Pop from the head while timestamp - q[0] >= window_ms. The window is half-open: a stored call exactly window_ms behind the new timestamp falls outside and frees a slot. Then if len(q) < max_requests, append and return True; otherwise return False without recording anything.
The solution
from collections import defaultdict, deque
class RateLimiter:
def __init__(self, max_requests, window_ms):
self.max_requests = max_requests
self.window_ms = window_ms
self.clients = defaultdict(deque)
def is_allowed(self, client_id, timestamp):
q = self.clients[client_id]
while q and timestamp - q[0] >= self.window_ms:
q.popleft()
if len(q) < self.max_requests:
q.append(timestamp)
return True
return False
def run_rate_limiter(operations):
limiter = None
results = []
for op in operations:
method, args = op[0], op[1:]
if method == "init":
limiter = RateLimiter(*args)
results.append(None)
else:
results.append(limiter.is_allowed(*args))
return resultsCommon follow-up questions
- Your `defaultdict` grows forever as new `client_id` values arrive. How do you garbage-collect clients that have gone quiet? (Tests memory hygiene in long-lived processes. A periodic sweep that prunes each deque and drops the entry when it is empty bounds memory to recently seen clients. Last-seen timestamps plus LRU eviction is the production answer.)
- How does this design change when N API servers behind a load balancer all need to share one limiter per client? (Tests Redis primitives. `ZADD` with the timestamp as score, `ZREMRANGEBYSCORE` to evict, and `ZCARD` to count, all inside a Lua script so the check-and-record is atomic across replicas. The deque-per-client design ports almost directly to a sorted set per client.)
- What breaks if the operations send timestamps that are not strictly increasing per client? (Tests robustness. With multiple servers, timestamps can arrive out of order. The deque assumes monotonic input, so an out-of-order call can land mid-deque and break the eviction loop. Server-side timestamping or sorted insertion fixes it.)