•CASE STUDY

Distributed Rate Limiter

8 min read·1,567 words·Intermediate

How to use this case study

SDE-2 / Mid

  • Be able to explain the token bucket algorithm clearly
  • Design the API and the Redis-based counter
  • Walk through what happens when a request is allowed or rejected

SDE-3 / Senior

  • Go deeper on sliding windows vs token bucket
  • Atomic updates with Lua scripts
  • Hot keys
  • What happens when Redis is slow or down (fail open vs fail closed)

Staff / Principal

  • Cover multi-region limits
  • Per-tenant quotas measured in cost units (e.g., LLM tokens)
  • Rule rollout
  • How to keep the limiter from becoming a single point of failure for the whole company

Problem RestatementProblem

Design a service that limits how often a client can call an API. For example: "each user can make at most 100 requests per minute" or "each company can use at most 1 million AI tokens per month". When a client goes over the limit, we reject the request with HTTP 429 (Too Many Requests) and tell them when to try again.

The tricky part is that our API runs on hundreds of servers. A user's requests can land on any of them, so the servers must share one view of how many requests the user has already made.

RequirementsRequirements

1.1 Functional

  • Limit by key: limit requests per user, per API key, per IP address, or per tenant (a tenant is one customer company).
  • Configurable rules: e.g., 100 requests/minute for free users and 1,000 requests/minute for paid users.
  • Allow short bursts: a user who was idle can send a few requests at once.
  • Clear response: return 429 with a Retry-After header saying how many seconds to wait.
  • Count cost, not just requests (variant): some limits count units such as LLM tokens or GB of storage.

1.2 Non-Functional

  • Very low latency: the check runs on every request, so it should add less than ~2 ms.
  • High availability: if the limiter breaks, the API should keep working.
  • Accuracy: small over-counting or under-counting is fine; letting someone send 10x their limit is not.
  • Scale: must handle the full traffic of the company.

1.3 Scale Estimates

  • 50 million daily users, peak traffic 500,000 requests per second.
  • Each request = 1 limiter check, so 500K checks/sec.
  • Each counter is tiny (key + count + timestamp ≈ 100 bytes). With 50M active keys that is about 5 GB, which fits in memory across a small Redis cluster.

1.4 API Design

The limiter is usually called internally, not by end users:

POST/v1/ratelimit/checkwith { key: "user:42", rule: "api_default", cost: 1 } → returns { allowed: true, remaining: 57, retry_after_ms: 0 }.
PUT/v1/ratelimit/rules/{rule}with { limit: 100, window_sec: 60, burst: 20 } → admin API to change a rule.

High-Level ArchitectureArchitecture

2.1 Overview

  • API Gateway: every request passes through here first. It calls the rate limiter before forwarding the request.
  • Rate Limiter logic: runs as a library inside the gateway (fastest) or as a small sidecar service.
  • Redis cluster: stores the counters so every gateway server sees the same numbers.
  • Rules service: stores the rules in a database and pushes changes to the gateways, which keep a local copy in memory.

2.2 Architecture Diagram

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
    C["Client"] --> GW["API Gateway + Rate Limiter"]
    GW -->|"check and update counter"| R[("Redis Cluster")]
    GW -->|"allowed"| S["Backend Services"]
    GW -->|"rejected"| C
    RS["Rules Service"] -->|"push rule changes"| GW
    RS --> DB[("Rules DB")]

Choosing an Algorithm

This is the heart of the interview. Here are the common options in simple terms:

AlgorithmHow it worksGoodBad
Fixed windowCount requests in each clock minute (12:00–12:01)Very simpleA user can send 100 at 12:00:59 and 100 at 12:01:00 = 200 in 2 seconds
Sliding window logStore the timestamp of every request, count the ones in the last 60sExactUses a lot of memory
Sliding window counterMix this minute's count with a weighted part of last minute's countAccurate enough, small memorySlightly approximate
Token bucketA bucket fills with tokens at a steady rate; each request takes a tokenAllows bursts, tiny memoryNeeds careful atomic updates
Our choice: token bucket. Think of a bucket that holds up to 20 tokens and gets 100 new tokens per minute (about 1.67 per second). Each request takes 1 token. If the bucket is empty, the request is rejected. This naturally allows short bursts (up to 20) while keeping the long-term rate at 100/minute.

We only need to store two numbers per key: tokens and last_refill_time. When a request comes in, we add the tokens earned since last_refill_time, then try to take one.

Data ModelData model

One Redis hash per key:

key:     rl:{rule}:{user_id}         e.g. rl:api_default:42
fields:  tokens = 12.4
         last_refill_ms = 1726740000123
TTL:     2 x window (so idle keys clean themselves up)

Rules (in a normal database, cached in memory on gateways):

CREATE TABLE rate_limit_rules (
  rule_name   TEXT PRIMARY KEY,
  limit_count INT,        -- tokens added per window
  window_sec  INT,
  burst       INT,        -- bucket size
  updated_at  TIMESTAMP
);

Key FlowsFlows

5.1 Checking a request

  1. Request arrives at the gateway. The gateway finds the rule for this route and user tier (from its in-memory copy).
  2. The gateway runs one Redis script (Lua) that refills tokens, checks if at least cost tokens are left, subtracts them, and returns the result. Running it as one script makes it atomic: two servers cannot both take the last token at the same time.
  3. If allowed, forward the request. If not, return 429 with Retry-After = (cost - tokens) / refill_rate.

5.2 Changing a rule

An admin updates a rule in the Rules Service. It saves the rule and publishes an event. Every gateway receives it and updates its local copy within a few seconds.

Deep Dive A — Counting requests across many serversDeep dive

Our API runs on hundreds of servers and a user's requests land on any of them. Getting to one shared, correct count is the whole problem.

Weak

Each server counts on its own

Every server keeps a counter in its own memory. It needs no network call and it is trivial to write.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  U["User - 100 req/min limit"] --> LB["Load balancer"]
  LB --> S1["Server 1 - counter 100"]
  LB --> S2["Server 2 - counter 100"]
  LB --> S3["Server 50 - counter 100"]
  S1 --> R["User actually gets 5,000 req/min"]
  S2 --> R
  S3 --> R

With 50 servers the user gets 50 times the limit, because no server sees the others' counts. It also resets whenever a server restarts or the fleet scales. Say this and move on: the counter has to be shared.

Good

One shared counter, read then write

Put the counter in Redis, keyed by user. Every server reads the count, decides, and writes the new value back. Now there is one number for everyone.

The bug is the gap between the read and the write. Two servers handling requests in the same millisecond both read tokens = 1, both allow, and both write tokens = 0. One token bought two requests. Under load this happens constantly, so the limit leaks by roughly the number of servers racing at that instant.

Best

One atomic operation per key

Do the refill, the check and the subtraction inside a single Redis Lua script. Redis runs one script at a time per key, so there is no gap for a second server to slip into:

-- KEYS[1] = bucket key, ARGV = now, rate, burst, cost
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(b[1]) or tonumber(ARGV[3])
local ts     = tonumber(b[2]) or tonumber(ARGV[1])
tokens = math.min(tonumber(ARGV[3]), tokens + (ARGV[1] - ts) * ARGV[2])
if tokens < tonumber(ARGV[4]) then return 0 end
redis.call('HMSET', KEYS[1], 'tokens', tokens - ARGV[4], 'ts', ARGV[1])
return 1

One round trip decides the request, so the check costs about 0.5–1 ms inside a data centre. Shard Redis by key with consistent hashing so each user's bucket lives on exactly one node — that keeps it to a single network call and lets the cluster grow with traffic.

Deep Dive B — Speed, hot keys and failuresDeep dive

  • Latency: a Redis call inside the same data center takes about 0.5–1 ms. That fits our budget.
  • Hot keys: one huge customer can send 50K requests/sec, all hitting one Redis key. Fix: give that customer a local token bucket on each gateway holding a slice of the limit (e.g., 50 gateways × 1/50 of the limit), and sync with Redis in batches every 100 ms. We trade a little accuracy for a lot of speed.
  • Redis is down or slow: we must choose.
  • Fail open (allow requests): the API keeps working, but nobody is limited for a while. Good for normal APIs.
  • Fail closed (reject requests): safer for expensive or abuse-prone endpoints such as login or payments.
  • Most teams fail open with a short timeout (e.g., 5 ms) and a fallback to a rough local limit.
  • Multiple regions: keep counters per region and give each region a share of the global limit. Truly global exact counting would need cross-region calls on every request, which is too slow.

Trade-offs & AlternativesTrade-offs

DecisionChoiceWhyAlternative
Where the limiter runsLibrary in the gatewayNo extra network hopSeparate service: easier to update, slower
Counter storeRedis clusterFast, atomic scripts, TTLIn-memory only: fast but each server sees different numbers
AlgorithmToken bucketHandles bursts, 2 numbers per keySliding window counter: smoother, no burst control
When Redis failsFail open + local fallbackKeeps the API upFail closed: safer for sensitive endpoints

Common Follow-up QuestionsFollow-ups

  • "How do you limit monthly quotas?" Use a plain counter per tenant per month (INCRBY with the cost), stored durably because losing it would reset someone's quota. Warn the customer at 80% and 100%.
  • "How do you limit LLM token usage when you don't know the cost up front?" Reserve an estimate before the call, then adjust with the real token count after the response.
  • "How do clients know their limits?" Return headers like X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset on every response.

Wrap-UpWrap-up

A good answer picks the token bucket, stores two numbers per key in a sharded Redis cluster, updates them atomically with a Lua script, and runs the check inside the API gateway. Then it explains the hard parts: hot keys, what to do when Redis fails, and why limits in multiple regions have to be approximate.

More Case Studies

Frequently Asked Questions

What is the Distributed Rate Limiter system design question?

Distributed Rate Limiter is a system design interview question asked at FAANG companies. It covers rate limiting, distributed systems, api design and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Distributed Rate Limiter question?

Anthropic, Apple, Atlassian, Goldman Sachs, Google, Meta, Microsoft, OpenAI, Oracle, Pinterest, Roblox, Salesforce, TikTok have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Distributed Rate Limiter interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Distributed Rate Limiter question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →