Skip to main content
← PROBLEMS / SYSTEM-DESIGN
A system design interview walkthrough

Design a Distributed
Rate Limiter.

A distributed rate limiter controls how many requests a user, API key, or IP address can make within a time window. It protects backend services from overload and abuse while keeping legitimate traffic available. The distributed challenge is sharing quota state across many API servers without adding too much latency.
Start the walkthrough30 min · 8 RESHADED stages
EDGELOGICSTORESYSTEM-DESIGN
REQUIREMENTS. ARCHITECTURE. SCALE.
Difficulty: mediumFocus: rate-limiting, cachingFramework: RESHADED 8-step guide
Less boilerplate. More understanding.
01 Learn the idea02 Follow the visual03 Know the limits
RESHADEDRequirements → Estimation → Storage → High-level design → API → Detailed design → Evaluation → Discussion.
R

Define the boundaries.

Requirements · 4 minutes
SAY THIS: “Before proposing a topology for Design a Distributed Rate Limiter, I’ll clarify the core functional requirements, burst characteristics, data volume, and availability SLOs to define rigid system boundaries.”
Core tension: single-server counters don't work across nodes; distributed counters add network round-trips. The 2 ms latency budget is tight — a Redis call is typically 0.5–1 ms in the same data center. Algorithm choice (token bucket, fixed window, sliding window log, sliding window counter) affects accuracy, memory, and implementation complexity.

What the system does

  • Enforce per-user and per-IP rate limits, such as 1,000 requests per minute per user.
  • Return HTTP 429 with retry guidance when a quota is exceeded.
  • Support per-second, per-minute, and per-hour windows.
  • Coordinate decisions across multiple API server instances.
  • Support configurable limits by API key tier and endpoint.
  • Define fail-open or fail-closed behavior when the rate-limit store is unavailable.

What to agree on

  • Peak API traffic is approximately 500,000 requests per second.
  • Rate-limit decisions should add no more than 2 ms at p99.
  • The limiter should provide shared enforcement across 10 or more API nodes in a region.
  • API processes do not share memory.
  • Regional enforcement is the default scope; strict global enforcement requires additional coordination.
E

Size the critical path.

Estimation · 3 minutes
SAY THIS: “Let’s establish order-of-magnitude sizing for peak throughput, memory footprint, and network ingress to size the critical latency path before making technology choices.”
100k+PEAK THROUGHPUT
≤ 2 msP99 LATENCY TARGET
10M+SCALE TARGET

At an illustrative 200 bytes per record/session, high-volume state requires dedicated primary in-memory partitions. Add replicas, allocator overhead, policy indexes, and headroom.

throughput = peak_qps × (1 + headroom_margin) state_ram ≈ active_entities × per_record_bytes × replication_factor

Always validate throughput and p99 latency targets under skewed traffic distributions and replica failovers.

S

Keep state shared and partitioned.

Storage · 3 minutes
SAY THIS: “I’ll separate low-latency ephemeral cache state from authoritative durable storage, ensuring strict consistency where needed and partitioning for scale.”

Cache / Ephemeral state

state:{partitionKey}:activeWindow

  • In-memory key-value cache (Redis Cluster / Memcached).
  • TTL expiration matching window validity bounds.
  • Hash tags to co-locate keys for atomic Lua scripts or transactions.

Durable / Authoritative store

  • Distributed NoSQL / relational database (DynamoDB, PostgreSQL).
  • Durable write-ahead log with replica failover.
  • Cache-aside pattern: read misses warm cache from durable store.
  • Asynchronous CDC / event streaming to telemetry sinks.

Local-only node state is not a shared cluster quota: N nodes could admit roughly N × the configured limits. Ensure state co-location and transactional boundaries align with partition keys.

H

Make the decision at ingress.

High-level design · 4 minutes
SAY THIS: “The gateway tier authenticates incoming traffic, validates payloads, and routes requests to decoupled services backed by partitioned caching and authoritative storage tiers.”
Sliding window counter backed by Redis. Each incoming request: compute window boundaries → INCRBY the current window counter for the key (userId:windowStart) → read previous window counter → compute weighted count = prevCount × (remaining fraction of prev window) + currCount → if weighted count > limit, return 429.

Use tight timeouts, circuit breakers, and rate-limiting middleware at ingress to protect downstream services from cascading collapse.

A

Define the contract.

API · 3 minutes
SAY THIS: “The API contract establishes idempotent mutation semantics, clear error taxonomy, and explicit status codes so upstream clients can safely retry.”
execute(requestId, payload, idempotencyKey) → { status, resourceId, metadata }

Success Path

Forward the request or return HTTP 200/201. Include standard tracking and rate/quota telemetry headers in the response.

Failure & Fallback

Use explicit HTTP status codes: 400 for invalid input, 429 for quota violations, and 503 with Retry-After during maintenance or partitions.

View example API payload contract
HTTP · APPLICATION CONTRACT
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=300

{
  "status": "success",
  "data": { "id": "res_98a7f", "processedAt": "2026-11-11T12:00:00Z" }
}

Include idempotency keys on write endpoints to guarantee at-most-once processing across network retries.

D

Make state mutations atomic.

Detailed design · 7 minutes
SAY THIS: “For the critical execution path, we enforce idempotency and concurrency controls using atomic primitives, avoiding lock contention and split-brain states.”
  1. Request arrives at API node with userId and timestamp.
  2. Compute current 60-second window start: windowStart = floor(now / 60) * 60.
  3. Redis MULTI: INCR user:{userId}:{windowStart}, GET user:{userId}:{windowStart-60}, EXPIRE user:{userId}:{windowStart} 120.
  4. weightedCount = prevWindowCount × ((windowStart + 60 - now) / 60) + currWindowCount.
  5. If weightedCount > limit: set X-RateLimit-Remaining: 0, return 429 with Retry-After header.
  6. Else: decrement or allow — return 200 with X-RateLimit-Remaining header.
STRATEGY / PATTERNWHY CHOOSE IT?TRADE-OFF / COST
Token bucket in Redis with Lua scriptatomic, avoids race conditions, handles burst naturally. Slightly more complex Lua script to maintain.Coordinate partition dependencies
Local in-memory limiter with gossip synclow latency (no network call) but requires approximate counting — nodes can each allow up to limit/N individually, risking N× burst.Coordinate partition dependencies
Centralized SQL counteraccurate but adds 5–20 ms per request — exceeds the 2 ms budget.Coordinate partition dependencies
View reference production code implementation
LUA · REFERENCE IMPLEMENTATION
-- Atomic Redis Token Bucket Script (Runs in single-threaded event loop)
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]

local rate = tonumber(ARGV[1])      -- Refill rate (tokens per second)
local capacity = tonumber(ARGV[2])  -- Maximum burst capacity
local now = tonumber(ARGV[3])       -- Current epoch timestamp (seconds)
local requested = tonumber(ARGV[4]) -- Tokens requested (typically 1)

local current_tokens = tonumber(redis.call('get', tokens_key))
local last_refreshed = tonumber(redis.call('get', timestamp_key))

if current_tokens == nil then
  current_tokens = capacity
  last_refreshed = now
end

local delta = math.max(0, now - last_refreshed)
local tokens = math.min(capacity, current_tokens + (delta * rate))

if tokens >= requested then
  redis.call('set', tokens_key, tokens - requested)
  redis.call('set', timestamp_key, now)
  redis.call('expire', tokens_key, 3600)
  redis.call('expire', timestamp_key, 3600)
  return 1 -- Allowed
else
  return 0 -- Denied (HTTP 429)
end

Avoid client-side read-modify-write loops without concurrency control. Use atomic scripts, row-level locks, or conditional mutations at the storage tier to guarantee linearizability.

View sequence trace & failure interaction flow
INTERACTION TRACE · SEQUENCE DIAGRAM
[ Clients ] -> [ API Gateway (Rate Limiter Middleware) ]
                     |                           |
                     v (Check Rule Cache)        v (Atomic Lua Script)
              [ Local In-Memory Cache ]   [ Redis Cluster Counter ]
E

Stress-test the design.

Evaluation · 6 minutes
SAY THIS: “The biggest operational risks involve node failover, partition skew, network isolation, and cascading downstream backpressure. Here is how we mitigate each.”
Sliding window vs fixed window

Fixed window allows up to 2× burst at the boundary (last second of window N + first second of window N+1). Sliding window eliminates this at O(1) memory cost.

Redis vs local counter

Local is O(0ms) but each node allows limit requests independently — 10 nodes allow 10× limit. Redis adds ~1 ms but provides a shared accurate counter.

Fail-open (allow on Redis failure)

Protects availability; risk: abuse during outage. Use for non-critical APIs or when abuse impact is low.

Fail-closed (block on Redis failure)

Protects against abuse; risk: legitimate users blocked during outage. Use for billing/sensitive APIs.

Lua scripts for atomicity

Redis MULTI/EXEC provides optimistic locking; Lua scripts provide true atomicity. For rate limiting, a Lua script is preferred to avoid race between INCR and GET.

Redis node failure

With Redis Sentinel or Cluster, replica promotion takes 10–30 s. During that window: fail-open (allow) or local fallback counter (allow up to local_limit = limit/node_count). Redis Cluster with consistent hashing for key distribution — each userId is pinned to one shard.

Hot user

A single userId key in Redis becomes a hot key, concentrating load on one Redis shard. Mitigation: replicate hot keys to N replicas and read-balance across them; or shard the counter across K sub-keys (userId:0..K-1) and sum them — introduces K reads per request.

Cross-region rate limiting

Each region maintains its own Redis counter. Eventual synchronization via async replication — allows up to sum(regional_limits) globally. True global rate limiting requires a global counter service (high latency) or token-bucket with quota allocation per region.

D

Close with the trade-offs.

Discussion · 5 minutes
SAY THIS: “In closing, we optimized for sub-millisecond p99 latency and high availability under partition. We trade strict distributed linearizability for predictable throughput and operational simplicity.”

If the interviewer asks…

  • Sliding window vs fixed window: Fixed window allows up to 2× burst at the boundary (last second of window N + first second of window N+1). Sliding window eliminates this at O(1) memory cost.
  • Redis vs local counter: Local is O(0ms) but each node allows limit requests independently — 10 nodes allow 10× limit. Redis adds ~1 ms but provides a shared accurate counter.

Keep the conclusion focused

  • What guarantees are strictly enforced at runtime?
  • What happens during network partitions or node failures?
  • Which CAP trade-off was intentionally chosen and why?
The 30-second summary

A distributed rate limiter controls how many requests a user, API key, or IP address can make within a time window. It protects backend services from overload and abuse while keeping legitimate traffic available. The distributed challenge is sharing quota state across many API servers without adding too much latency.

“Put the limiter at the API gateway, keep policy configuration close to the request path, and use an atomic shared counter store such as Redis. Choose the algorithm, consistency scope, and outage behavior explicitly.”

Interactive knowledge check

Atomic Counter Rate Limiting Under Concurrency

Why are standard Redis GET and SET operations unsafe for implementing API rate limiting across multi-threaded gateway nodes?

Recommended Practice Flow← All Problems Catalog