Interview Cracker GitHub

Worked Example: Design an API Rate Limiter

A complete worked answer for "design the rate limiter in front of a public API," start to finish. The goal is to show the shape of a good answer: clarify, estimate, pick a data structure on purpose, design the data model, walk the request path, and name the bottlenecks you would actually hit.


1. Clarify requirements

The honest scoping line: "I'll design a fixed-window counter first because it is simple and good enough, then show the upgrade to sliding window and token bucket and when each is worth it."


2. Estimate the scale

The key insight: this is a read-mostly, per-key, low-latency workload. The data is small and hot. That argues strongly for an in-memory, sharded design rather than a database on the hot path.


3. The core data structure

Three algorithms, in increasing order of fairness and cost:

Decision to state out loud: fixed-window counter for v1 (simple, shardable, O(1)); token bucket if the spec allows bursts; sliding-window counter if the boundary burst is a real problem and we want to stay O(1).


4. Data model

Per key, store the minimum state the chosen algorithm needs:

limiter_state  (in memory, sharded by key)
  key           STRING   (API key / user id)  -- partition key
  window_start  TIMESTAMP
  count         INT8/INT32
  -- token bucket variant instead:
  tokens        FLOAT    (current bucket level)
  last_refill   TIMESTAMP

State is small and hot → keep it in memory on the limiter nodes, sharded by hash(key). Persistence is a reliability concern, not a correctness one: if a limiter node dies we can lose that shard's counters and briefly under-count — acceptable for a limiter (failing open for a few seconds beats failing closed and rejecting everyone). Optionally replicate the hot shard to a backup node so a failure is a sub-second failover rather than a reset.


5. The architecture

                +------------------+
   request ----> |   edge / LB      |
                +--------+---------+
                         |
                +--------v---------+        shard by hash(key)
                |  rate limiter    |  ---->  [shard 0] [shard 1] ... [shard N]
                |  (stateful,      |         in-memory counters / token buckets
                |   in-memory)     |
                +--------+---------+
                         | admit / 429
                +--------v---------+
                |   API servers    |
                +------------------+

6. The request path in detail

  1. Edge extracts the identity (API key / auth token) and hashes it → shard.
  2. That shard computes the decision: - fixed window: if now crossed into a new window, reset count; then count < limit ? admit : reject. - token bucket: refill tokens += R * (now - last_refill) (capped at B), then tokens >= 1 ? (tokens -= 1; admit) : reject.
  3. Admit → forward to the API servers. Reject → return 429 + Retry-After without touching the API servers (that is a feature: the limiter is the first line of defence against a flood).

The invariant to name: the check-and-increment must be atomic per key. Within one shard that is a single-threaded or lock-protected counter — no race. Across shards a key always maps to exactly one shard, so there is no cross-shard race. This is why "shard by key" is load-bearing, not just a scaling trick.


7. Bottlenecks and what you would do next


8. What the interviewer is grading