CMD Guide
HomeSystem DesignSystem Design Problems

Designing an API Rate Limiter

1. What is a Rate Limiter?

A rate limiter caps how many requests a sender can issue in a specific time window. Once the cap is reached, further requests are blocked or throttled. Examples include:

2. Why do we need API Rate Limiting?

Rate limiting protects services from abusive behavior at the application layer, such as denial-of-service attacks, brute-force password attempts, and brute-force credit card transactions. It also helps:

3. Requirements and Goals

Functional:

Non-functional:

4. How to do Rate Limiting?

Rate limiting defines the rate and speed at which consumers can access APIs. Throttling controls usage during a given period and can be applied at the application or API level. When a throttle limit is crossed, the system can reject, queue, or degrade the request.

5. Types of Throttling

6. Algorithms for Rate Limiting

Four algorithms cover almost every production rate limiter. The choice is a trade-off between accuracy, memory, burst tolerance, and downstream protection.

Algorithm decision table

AlgorithmMemoryBurst behaviorBest forAvoid when
Fixed windowO(1)Up to 2× limit across a boundarySimple, coarse limitsStrict rate enforcement near window edges
Sliding windowO(window)Exact; never exceeds limitStrict enforcementMemory is constrained
Token bucketO(1)Bursts up to capacity, then rate-limitedUser quotas, APIs that can absorb burstsDownstream cannot tolerate any burst
Leaky bucketO(1)Queue or reject; output is flatProtecting fixed-capacity downstreamYou want to serve legitimate bursts quickly

The token bucket and the leaky bucket admit the same number of requests under the same limits, but the timing differs: a token bucket serves a burst immediately, while a leaky bucket spreads the admitted requests out at the leak rate. See the interactive walkthrough at the end of this page for a side-by-side trace.

7. High-level Design

When a request arrives, the web server asks the rate limiter whether to serve or throttle it. If the request is allowed, it proceeds to the API servers; otherwise it is rejected with an appropriate status code.

8. API Contract

The rate limiter is consulted before a request reaches the API server. A minimal design exposes it as middleware or an internal service:

Example request trace

Limit: 3 requests per minute for user 42 on the createURL API. Redis fixed-window counter keyed by minute bucket.

StepRequestRedis actionResult
1POST /createURL by user 42 at 14:05:01INCR rate:42:createURL:14:05 → 1, EXPIRE 60sHTTP 200, remaining 2
2POST /createURL by user 42 at 14:05:20INCR → 2HTTP 200, remaining 1
3POST /createURL by user 42 at 14:05:40INCR → 3HTTP 200, remaining 0
4POST /createURL by user 42 at 14:05:55INCR → 4HTTP 429, retry after 5s
5POST /createURL by user 42 at 14:06:01New minute-bucket key (…:14:06); INCR → 1HTTP 200, remaining 2

9. Basic System Design and Algorithm

Suppose we want to limit each user to three requests per minute. For each user we store a counter and the start time of the current window. On each request:

  1. If the user is not in the store, insert them with count 1 and start time now, then allow.
  2. If CurrentTime - StartTime >= 1 minute, reset the start time and count to 1, then allow.
  3. If CurrentTime - StartTime < 1 minute and Count < 3, increment count and allow.
  4. If Count >= 3, reject.

Problems:

Atomicity with Redis: If Redis stores the counter, avoid a separate distributed lock for the read-update cycle. Locks serialize requests from the same user and add latency. Instead, use an atomic Redis operation such as a Lua script or INCR with TTL. A Lua script can read the current window, reset it if expired, and increment the counter in a single server-side execution, eliminating the race without a lock.

10. Sliding Window Algorithm

Store the timestamp of each request in a Redis sorted set per user. For each new request:

  1. Remove timestamps older than CurrentTime - window.
  2. If the set size is greater than or equal to the limit, reject; otherwise insert the current timestamp and allow. (With > alone, a set already holding exactly limit timestamps would admit one more — limit + 1 requests in a window.)

This is accurate but memory-intensive because every request is stored: at a limit of 500 requests/hour, 500 × (8-byte timestamp + ≈20 bytes of sorted-set overhead) ≈ 14 KB per user — ~14 GB for one million active users.

11. Sliding Window with Counters

Keep counts for smaller sub-windows, e.g., one count per minute for an hourly limit. When a request arrives, sum the counts in the relevant sub-windows to estimate the current rolling count. Store counters in a Redis hash with an expiration time. This uses far less memory than storing every timestamp.

Memory estimate

For a limit of 500 requests/hour with one-minute sub-windows, a user needs at most 60 counters. A counter (2 bytes) plus a normalized timestamp (4 bytes) plus Redis hash overhead (≈20 bytes) gives about 1.6 KB per user, or ~1.6 GB for one million active users — roughly an order of magnitude less than storing every request timestamp.

12. Data Sharding and Caching

Shard state by UserID to distribute load. For fault tolerance, replicate shards with consistent hashing. If different APIs need different limits, shard per user per API.

Caching recent active users in application servers reduces backend lookups. A write-back cache, where counters are updated in cache and written to the store asynchronously, is fast but can lose state if the cache crashes before flushing. For rate limiting, that may mean allowing more requests than intended after a failure. Use write-through or periodic persistence if strict enforcement matters, and design the cache to recover quickly from restarts.

13. Should we rate limit by IP or by user?

14. Failure mode: what if the limiter backend is down?

A centralized rate limiter usually depends on a shared Redis instance. If Redis becomes unreachable, the limiter has two extreme answers and one practical middle ground:

The right default depends on the endpoint: fail-closed for correctness or cost boundaries (e.g., a paid third-party API you are billed per call for); fail-open or local-fallback for availability-sensitive read paths. Either way, alert loudly when the fallback path is active — both “unenforced” and “rejecting everything” are incidents, not steady states.

15. Interactive walkthrough

Step through fixed, sliding, token-bucket, and leaky-bucket behavior on the same bursty traffic patterns.

Prediction drill

Before you run the simulator, write down your prediction for each scenario. The goal is not the exact number — it is to notice where each algorithm surprises you.

ScenarioWhat to predict
Token bucket: capacity 10, refill 1/sec, 15 requests arrive at t=0.How many are accepted? When can the 16th request be served?
Leaky bucket: leak rate 2/sec, capacity 5, 8 requests arrive in 1 second.How many are rejected? Sketch the output rate over the next 3 seconds.
Fixed window: limit 3/minute, 3 requests at 14:05:59 and 3 at 14:06:01.How many total requests are accepted in those 2 seconds? Why is this a problem?
Distributed token bucket: two app servers each keep a local copy.With no sync, can a user burst to 2× the global limit? How do you fix it?

After running the simulator: compare your predictions to the trace. The fixed-window boundary burst and the distributed-token-bucket race are the two traps that most often break production limiters.

🪜 Drill ladder: rate-limiter follow-ups

1. Boundary burst

Question: a fixed window of 100 requests/minute sees 100 requests at 14:05:59 and 100 requests at 14:06:00. Is this allowed?

Answer: yes, and it is usually wrong. A fixed window can admit 2× the intended rate across a one-second boundary. If that matters for your downstream, use a sliding window or a token bucket with capacity ≤ the steady-state burst you actually want to allow.

2. Distributed state

Question: you shard rate-limit state by user ID across Redis instances. One Redis node fails. What happens to users whose state was on that node?

Answer: without replication, those users are unprotected (fail-open) or locked out (fail-closed) until the node recovers. The better design replicates the counter or uses a consensus-backed store (e.g., Redis Cluster with replicas + failover) and a local fallback. The choice depends on whether the endpoint protects money (fail-closed) or availability (fail-open/fallback).

3. Fail-open vs fail-closed

Question: when is fail-closed the right answer?

Answer: fail-closed is correct when the limiter guards a finite, billable, or safety-critical resource: paid API calls, SMS sends, inventory reservations, or medical-device requests. Fail-open is correct when the limiter is a best-effort guardrail on an availability-critical read path, and rejecting everything would cause a larger outage than a temporary abuse spike. Document the choice per endpoint; do not let it be accidental.

Rate-limit headers: a worked example

When you reject or throttle a request, tell the client how to behave. The de-facto standard headers are:

HeaderMeaningExample
X-RateLimit-LimitMaximum requests allowed in the current window.100
X-RateLimit-RemainingRequests left in the current window.87
X-RateLimit-ResetUnix timestamp when the window resets.1719000000
Retry-AfterSeconds (or HTTP-date) to wait before retrying.5

Trace: user 42 is limited to 3 requests/minute on POST /createURL.

Client contract: a well-behaved client reads Retry-After and sleeps; a naive client that ignores it will be throttled again and may be penalized with exponential backoff on the server side.

Takeaways


Adapted from DesignGurus and standard rate-limiting practice. Re-authored and corrected for this guide. See also: Rate Limiting Algorithms: Token Bucket vs Leaky Bucket, System Design Problems II — Resolution Mechanisms.

16. Multi-region enforcement and where to place the limiter

Where the limiter lives. Two placements, and you usually want both: at the edge/API gateway for coarse per-IP / per-API-key limits (cheap, protects the whole fleet, stops floods before they cost you anything), and at the service/mesh layer for per-tenant business quotas that need application context (plan tier, per-endpoint cost). Edge limits are the blunt instrument; app-level limits know what the request actually costs.

The multi-region overshoot trap. If each region enforces the limit against its own local store, the global limit is silently multiplied by the number of regions. Concretely: a 100 req/min limit enforced independently in 3 regions admits up to 3 × 100 = 300 req/min globally for a user whose traffic is spread across them. You have three honest options:

The point an interviewer probes: naming that distributed enforcement is a consistency-vs-latency choice, and that "just use one Redis" trades your latency SLO for exactness. Recompute the overshoot for the region count in the prompt before you pick.

🔨 Practice this hands-on — Design a Distributed Rate Limiter →
Attempt it from an empty file, break it to feel the failure, then defend it under pushback.
🤖 Don't fully get this? Learn it with Claude

Stuck on Designing an API Rate Limiter? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Designing an API Rate Limiter** (System Design) and want to truly understand it. Explain Designing an API Rate Limiter from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Designing an API Rate Limiter** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Designing an API Rate Limiter** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Designing an API Rate Limiter** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes