Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°31

Sliding-Window Rate Limiter

Medium SystemsRate LimitingQueue

Allow at most N requests per rolling time window — the rate limiter that guards real APIs. A sliding log of accepted timestamps gives exact limits without the boundary bursts a fixed window suffers. Decide accept/reject for a stream of requests. Solve it in Python or TypeScript, with hidden tests.

The problem

Implement a rate limiter that permits at most limit requests in any window of window time units. You are given the request timestamps in non-decreasing order. For each timestamp, return True if the request is allowed (and it then counts against the limit) or False if it is rejected. A request at time t is allowed iff fewer than limit accepted requests fall in the half-open window (t - window, t].

SOLVE IT YOURSELF

Your turn — write it

Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.

YOUR TASK

Implement rate_limiter(limit, window, timestamps): keep a queue of accepted timestamps. For each new t, evict entries ≤ t - window from the front, then allow (and enqueue) if the queue holds fewer than limit; otherwise reject.

CPython · WebAssembly
Examples and constraints 4 examples
EXAMPLE 1
Input limit = 2, window = 10, timestamps = [1, 2, 3, 11, 12]
Output [True, True, False, True, True]
the 3rd (t=3) is the 3rd request in (−7,3] → rejected; by t=11 the t=1 request has aged out
EXAMPLE 2
Input limit = 1, window = 5, timestamps = [1, 2, 6]
Output [True, False, True]
t=2 is within 5 of t=1; t=6 is not (6−5=1, and 1 ≤ 1 ages out)
EXAMPLE 3
Input limit = 1, window = 5, timestamps = [1, 2, 3, 4, 6]
Output [True, False, False, False, True]
the rejects at 2, 3 and 4 are not logged, so t=6 only has to outlast t=1
EXAMPLE 4
Input limit = 1, window = 5, timestamps = [0, 5]
Output [True, True]
edge case: the request at 0 is exactly 5 old at t=5 and has left the half-open window
CONSTRAINTS
  • Timestamps are non-decreasing; multiple requests may share a timestamp.
  • A rejected request does not count toward the limit (only accepted ones occupy the window).
  • The window is half-open: a request exactly window units old (t - window) has left the window.
  • Aim for amortized O(1) per request by dropping expired entries from the front of a queue.
Hints 4 ideas
HINTS — 4 IDEAS
  1. Only accepted requests occupy the window, so store just those in a queue (a deque).
  2. Before deciding t, pop every front entry ≤ t - window — those have slid out of the window.
  3. If the queue length is now below limit, accept: append t and return True. Otherwise return False.
  4. The half-open boundary matters: use ≤ t - window (not <) so an entry exactly window old is expired.
Approach, complexity & discussion — open after you solve

Start from brute force

The obvious approach keeps every accepted timestamp in a plain list and, for each new request at t, counts the entries greater than t - window; accept and append if that count is below limit. It is correct, because it measures the half-open window directly. But it rescans the whole history on every request and never forgets anything, so it costs O(n²) time in the worst case (every request accepted, each scanning all earlier ones) and O(n) space. A million requests under a high limit means on the order of 5 × 10¹¹ comparisons, all to re-check timestamps that left the window long ago.

The approach

The insight is that the window only ever slides forward. Timestamps arrive in non-decreasing order, so once an accepted request is window units old it can never count again, and the expired requests are always the oldest ones. That makes a FIFO queue exact: evict from the front, append at the back, and what remains is precisely the accepted requests in (t - window, t].

The reference keeps log, a deque of accepted timestamps, oldest first. For each t, the loop while log and log[0] <= t - window pops everything that has slid out; the <= is what makes the window half-open, so a request exactly window old is gone. Then len(log) < limit decides: accept, append t and record True; otherwise record False and leave log untouched, because rejected requests do not occupy the window. The TypeScript version gets the same effect by advancing a head index instead of popping.

Walk through an example

The first example, limit = 2, window = 10, timestamps = [1, 2, 3, 11, 12], one row per request.

steptt − windowevictedlog afterdecision
11−9—[1]0 < 2, True
22−8—[1, 2]1 < 2, True
33−7—[1, 2]2 is not below 2, False (not logged)
41111 (1 ≤ 1)[2, 11]1 < 2, True
51222 (2 ≤ 2)[11, 12]1 < 2, True
return————[True, True, False, True, True]

Complexity

Deque log: O(1) amortized per request, because each accepted timestamp is appended once and popped at most once, so n requests cost O(n); space O(limit), since an entry is logged only while fewer than limit are present. Brute-force scan: O(n²) time in the worst case, because every request rescans the full history, and O(n) space, since nothing is evicted.

Edge cases to test

  • A burst at one instant, limit = 2, window = 10, [5, 5, 5]: [True, True, False]. Equal timestamps all sit inside the same window.
  • The exact boundary, limit = 1, window = 5, [0, 5]: [True, True]. At t = 5 the entry 0 is exactly window old and is evicted (0 ≤ 0).
  • Rejects do not count, limit = 1, window = 5, [1, 2, 3, 4, 6]: [True, False, False, False, True]. If the rejected requests were logged, t = 6 would be rejected too.
  • limit = 0, [1, 2]: [False, False]. Nothing is ever allowed.
  • A long gap, limit = 2, window = 10, [1, 2, 100]: [True, True, True]. One eviction loop drains every expired entry at once.

Common mistakes

  • Falling back to a fixed window, whose boundary lets a client send up to 2× the limit across the edge — the very thing sliding windows fix. With limit = 2 and 10-unit buckets, requests at 8, 9, 10 and 11 are all accepted, four inside the span (1, 11].
  • Unbounded timestamp growth in the log variant (evict old entries).
  • Logging rejected requests as well: rejections then keep the window full, and rate_limiter(2, 10, [1, 2, 3, 11, 12]) rejects t = 11 and t = 12 because t = 3 is still in the log.
  • Evicting with < instead of <=: an entry exactly window old stays, and rate_limiter(1, 5, [0, 5]) returns [True, False].
  • Accepting while len(log) <= limit: an off-by-one that lets limit + 1 requests into every window.

Follow-ups an interviewer asks next

One client sends millions of requests per window. Can you avoid storing timestamps?
Use a sliding-window counter: keep only the counts for the current and the previous fixed bucket, and estimate the rolling count as the current count plus the previous count times the fraction of the previous bucket still inside the rolling window. That is O(1) memory per client instead of O(limit), at the price of assuming the previous bucket’s requests were spread evenly, so the estimate can be off near a burst.
Enforce the limit per user across many API servers.
Keep each user’s log in a shared store, such as a Redis sorted set per user: remove members scored at or below t − window (ZREMRANGEBYSCORE), count the rest (ZCARD), and ZADD the request only if under the limit, all in one Lua script so two servers cannot both pass the check. Members must be unique, so add a request id scored by its timestamp, and expire idle keys.
Rejected requests should count, to punish clients that retry in a tight loop.
Log every request instead of only the accepted ones. A client that keeps retrying while blocked then keeps its own window full and stays blocked until it pauses for a full window. That is a policy choice, not a fix: the version here, where rejects are free, lets a client recover as soon as its oldest accepted request ages out.

Where this shows up

Sliding-window rate limiting is what you reach for when a fixed window’s edge burst is unacceptable — it makes “100 per minute” actually mean 100 in any 60-second span. It is standard in API gateways and abuse prevention where the limit has to hold across boundaries, not just within neat clock minutes.

Reference solution

Reference solution — Python
from collections import deque

def rate_limiter(limit, window, timestamps):
    log = deque()      # timestamps of ACCEPTED requests, oldest at the front
    out = []
    for t in timestamps:
        while log and log[0] <= t - window:   # slide the window forward
            log.popleft()
        if len(log) < limit:
            log.append(t)                     # accept: it now occupies the window
            out.append(True)
        else:
            out.append(False)                 # rejected requests do not count
    return out


print(rate_limiter(2, 10, [1, 2, 3, 11, 12]))  # -> [True, True, False, True, True]
Reference solution — TypeScript
function rateLimiter(limit: number, window: number, timestamps: number[]): boolean[] {
  const log: number[] = [];   // accepted timestamps, oldest first
  let head = 0;               // index of the front (amortized O(1) dequeue)
  const out: boolean[] = [];
  for (const t of timestamps) {
    while (head < log.length && log[head] <= t - window) head++;  // slide window
    if (log.length - head < limit) {
      log.push(t);            // accept: now occupies the window
      out.push(true);
    } else {
      out.push(false);        // rejected: does not count
    }
  }
  return out;
}

console.log(rateLimiter(2, 10, [1, 2, 3, 11, 12])); // -> [true,true,false,true,true]

Questions about Sliding-Window Rate Limiter

What is the difference between a fixed window and a sliding window rate limiter?

A fixed window counts requests per calendar bucket, such as each clock minute, and resets the count at every boundary. That is cheap, but a client can send the full limit at the end of one bucket and the full limit again at the start of the next, so twice the limit lands inside one window-length span. A sliding window counts the requests in the window units just before each request, whatever the clock says, so the limit holds over every span. The log version in this challenge is exact; the counter version approximates it with two buckets.

Why does the rate limiter evict a timestamp that is exactly window units old?

Because the window is half-open, (t - window, t]: it includes the current instant and excludes the instant exactly window units earlier. With a window of 5, a request at time 0 stops counting at time 5, so rate_limiter(1, 5, [0, 5]) allows both. That is why the reference evicts while log[0] is at or below t - window, not strictly below it. The convention means a client allowed one request per 5 units can send at 0, 5, 10 and so on, exactly on the beat, without ever being rejected.

How much memory does a sliding window log rate limiter use?

At most limit timestamps per client, because a request is logged only while fewer than limit are already in the window, and expired entries are popped as the window slides. For a limit of 100 per minute, that is up to 100 numbers per client, however many requests the client sends. The cost grows with the limit and with the number of clients, which is why high-limit deployments often switch to a sliding window counter, which keeps just two counts per client and approximates the rolling total.

Why use a deque instead of a list for the request log?

Because the log is consumed from the front. In Python, list.pop(0) shifts every remaining element and costs time proportional to the list length, while deque.popleft() takes constant time. Each accepted timestamp is appended once and popped at most once, so with a deque the whole run is O(n) for n requests. The TypeScript reference reaches the same bound another way: it keeps a head index into an array and advances it instead of removing elements, which is fast but leaves the consumed entries in memory unless the array is compacted.

Finished this one? 0 / 76 Challenges done
NextCircuit Breaker Next on the “Systems, limits & distribution” ladder · rung 5 of 9

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Challenges