Rate Limiter

Build a fixed-window rate limiter, replace it with a sliding window that closes the boundary burst, then enforce several rules at once and implement a token bucket.

Also known as: Sliding window rate limiter, Fixed window rate limiter, API rate limiter

Why practise this

Rate limiting and time-window counts shows up in 6.1% of the 1,447 coding interview reports we studied, at 10 companies. Allow or reject requests, or count events, within a moving time window.

Reported at: OpenAI, Rippling, Harvey AI, Snowflake, Zip, Anthropic, Citadel, Waymo.

This is an original practice problem written by AceOffer to train the pattern. It is not a question any company asked.

What candidates report interviewers add

Follow-ups candidates reported on questions with this pattern, in our words. Parts marked “from reports” below are built on them.

Design a rate limiter that decides whether each incoming request from a client is allowed. Time is an integer number of seconds passed in as now, and calls arrive with now never decreasing. In every part, only allowed requests count toward a limit; rejected ones are not recorded.

Questions to ask before you code

  • Do rejected requests count against the client?
  • Is the window boundary inclusive: does a request at t still count at t + window?
  • With several rules, if one rule rejects a request, should the others still count it?
  • Should bursts be allowed (token bucket) or should the count over any window be strict (sliding window)?
  • Is the limit per client, per group, or both?

Part 1: Fixed window

Implement RateLimiter(limit, window) with limit >= 1 and window >= 1.

Time is split into fixed windows [0, window), [window, 2 * window), and so on. allow(client, now) returns True if the client has made fewer than limit allowed requests in the window that contains now; that request then counts. Otherwise it returns False. Each client is counted separately, and the count starts from zero in every new window.

Part 2: Sliding window

A fixed window lets a client send limit requests at the end of one window and limit more at the start of the next, so twice the limit in a moment.

Implement SlidingWindowRateLimiter(limit, window) with the same allow(client, now). A request is allowed if the client has fewer than limit allowed requests at times t where now - window < t <= now. So a request made at time t stops counting at exactly t + window.

Part 3: Several rules at oncefrom reports

Real limits come in groups: "3 per user per minute" plus "10 per team per minute", or "3 per second" plus "20 per 10 seconds" for the same user.

Implement MultiRuleRateLimiter(rules). rules is a list of (field, limit, window) tuples. Each rule is a Part 2 sliding window counted separately for each value of request[field]. Two rules may use the same field with different windows.

allow(request, now) takes a dict that has every field the rules name, for example {"user": "u1", "team": "t1"}. The request is allowed only if every rule has room for it. If it is allowed it counts toward every rule; if any rule rejects it, it counts toward none of them.

Part 4: Token bucketfrom reports

Implement TokenBucketRateLimiter(capacity, refill_rate), a different algorithm that allows short bursts while holding an average rate.

  • Each client has a bucket holding up to capacity tokens. A client's bucket is full the first time it is seen.
  • The bucket gains refill_rate tokens for each second that passes and is capped at capacity. You do not need a timer: when a call arrives at now, add (now - last call's now) * refill_rate tokens, capped at capacity.
  • allow(client, now, cost=1) returns True and removes cost tokens if the bucket holds at least cost. Otherwise it returns False and removes nothing.

All values are integers. Clients have separate buckets.

Open this page on a laptop or desktop to write and run your code against the tests in the browser.

Approach

Spoilers: try each part before reading its walkthrough.

Part 1: Fixed window

A fixed window limiter only needs two numbers per client: which window it is in and how many allowed requests it has made there. The window number is now // window, so seconds 0 to 9 share window 0 when the window is 10. On each call, if the stored window number differs from the current one, the count starts again from zero. If the count has reached the limit the request is rejected and nothing changes; otherwise the count goes up by one. Rejected requests must not increase the count, or a client that keeps retrying would stay blocked. This is O(1) time and O(1) memory per client, which is why fixed windows are popular. The weakness is at the edges, which Part 2 fixes.

Part 2: Sliding window

With fixed windows a client can spend its whole limit at the end of one window and again at the start of the next. A sliding window removes that burst by asking, at each request, how many allowed requests happened in the window seconds ending now. Keep each client's allowed request times in order. Before checking, drop every time at or before now - window, since those can never count again; the reference solution finds the cut with bisect_right and deletes the prefix. If fewer than limit times remain, append now and allow the request. Each time is added and removed once, so the cost is amortized small, and memory is at most limit times per client. The price compared with Part 1 is that memory grows with the limit. The reference also splits allow into has_room and record, which Part 3 reuses.

Part 3: Several rules at once

Each rule is just a Part 2 sliding window whose key is the value of one request field, so the reference builds one SlidingWindowRateLimiter per rule and keeps them in a list. Rules that share a field still get their own window objects, which is what lets a per-second rule and a per-10-second rule coexist for the same user. The subtle requirement is atomicity: a request rejected by any rule must leave no trace in the others. So allow runs in two passes. First it asks every rule whether it has room, stopping at the first refusal; only if all agree does it record now in every rule. Recording as you go would let the user rule count a request that the team rule then rejects. The cost is O(R log L) per call for R rules.

Part 4: Token bucket

A token bucket keeps two numbers per client: tokens left and the time of the last call. Instead of a timer, each call refills lazily: the tokens earned since the last call are (now - last) * refill_rate, and the bucket is capped at capacity, so a long idle period cannot bank more than one full burst. Then, if at least cost tokens remain, subtract them and allow the request; otherwise reject it and leave the tokens alone. New clients start with a full bucket. Compared with the sliding window this uses constant memory per client and naturally supports requests of different sizes, but it limits the average rate plus a burst rather than giving a strict count over every window. Each call is O(1).

Hints

Part 1 hint

Keep, per client, the current window number (now // window) and a count. When the window number changes, start the count over.

Part 2 hint

Keep the times of each client's allowed requests in order. Drop the ones at or before now - window, then compare how many remain with the limit.

Part 3 hint

Split the Part 2 check into two steps, 'is there room?' and 'record it', so a multi-rule request can ask every rule first and record only when all agree.

Part 4 hint

Store just two numbers per client: tokens left and the time of the last call. Refill lazily at the start of each call instead of running a timer.

Common mistakes

  • Recording rejected requests, so a client that keeps retrying never gets through.
  • Off by one at the edge of the window: a request at t should stop counting at exactly t + window.
  • Recording a request in the rules that passed before discovering a later rule rejects it.
  • Sharing one log across rules that use the same field but different windows.
  • Letting the bucket refill past capacity after a long idle period, or spending tokens on a request that is then rejected.

Where an interviewer could take it next

  • Return how many seconds a rejected client should wait before retrying.
  • Approximate the sliding window with the current and previous fixed-window counts to use O(1) memory per key.
  • Let new rules be added as plug-ins without changing the limiter's core loop.

Complexity

The fixed window limiter is O(1) time and memory per client. Each sliding window keeps at most limit times per key; each time is appended once and removed once, so a check is amortized O(log L). The multi-rule limiter does that once per rule, O(R log L) for R rules. The token bucket is O(1) time and two numbers of memory per client.

Frequently asked

What is the difference between a fixed window and a sliding window rate limiter?
A fixed window limiter counts requests in set blocks of time, such as each whole minute, and resets the count at every boundary. It is cheap, but a client can send its full limit just before a boundary and again just after. A sliding window limiter counts requests in the period ending at the current moment, so that double burst is impossible. It costs more memory, because it remembers recent request times.
How does a token bucket compare to a sliding window rate limiter?
A token bucket holds up to a fixed number of tokens and refills at a steady rate. Each request spends one token and is rejected when the bucket is empty. It stores only a token count and a last-refill time per client, and it allows short bursts up to the bucket size while enforcing an average rate. A sliding window gives a strict count over the last N seconds instead, at the cost of storing request times.
How do you calculate retry-after for a rate limiter?
With a sliding window, list the allowed request times still inside the window. If fewer than the limit remain, the client can retry now. Otherwise find the request whose expiry brings the count below the limit: with c requests and limit L, it is the one at index c - L counting from the oldest. The wait is that request's time plus the window length minus the current time.

Practise next

See what candidates report at each company

The company guides collect the questions candidates report, with every part, the follow-ups interviewers added, and how often each one comes up.

Browse the interview guides
Is this helpful?