Low-level design
Designing a rate limiter
A gatekeeper that decides, in under a millisecond, whether a request gets to proceed or gets turned away — protecting shared resources from being overwhelmed by any single client, on purpose or by accident.
Strategy pattern · middleware · distributed systems · concurrency
Requests arrive, the gate decides
Understanding & Motivation
Before designing anything, articulate *why* a system needs rate limiting. Interviewers are listening for whether you connect the mechanism to real failure modes, not just recite algorithm names.
DoS / abuse prevention
Blocks a single bad actor (or buggy retry loop) from monopolizing capacity meant for everyone else.
Resource starvation
Protects finite resources — DB connections, thread pools, third-party quotas — from being exhausted by traffic spikes.
Cost optimization
Every request often has a real dollar cost (compute, egress, downstream API calls) — limiting caps the bill.
Cascading failure mitigation
Stops overload in one service from rippling upstream and taking down every dependent service with it.
Illustrative real-world limits (rounded, publicly documented order-of-magnitude)
| Service | Typical Limit | Why this shape |
|---|---|---|
| GitHub REST API | ~5,000 req/hour (authenticated) | Long window, high ceiling — optimized for scripts & CI, not bursts. |
| Stripe API | ~100 req/sec (live mode) | Short window — protects a payments-critical path from spikes. |
| Social APIs (e.g. X/Twitter) | Tiered, per 15-min window | Per-endpoint tiers so expensive reads are throttled harder than cheap ones. |
Requirements Gathering
Split requirements into functional (what it does) and non-functional (how well it does it) — interviewers explicitly score this separation.
Functional Requirements
- Allow or block a request based on a configurable threshold.
- Support scoping by client ID, IP address, API token, or user tier.
- Different limits per route/endpoint (e.g. cheap read vs. expensive write).
- Give real-time feedback via standard response headers.
- Return
429 Too Many Requestswith aRetry-Afterhint.
Non-Functional Requirements
- Low latency overhead — check should add <2ms to the request path.
- Memory-efficient — ideally O(1) state per client, not O(requests).
- Horizontally scalable — correct even across many stateless API instances.
- High availability — the limiter must never become the outage.
- Graceful degradation — decide fail-open vs. fail-closed if the store is down.
Standard response contract
Success — headers on every response
HTTP/1.1 200 OK X-RateLimit-Limit: 100 X-RateLimit-Remaining: 42 X-RateLimit-Reset: 1755392400
Denied — limit exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Remaining: 0
{"error":"rate_limit_exceeded"}
Design Patterns Applied
A rate limiter is a great vehicle for demonstrating pattern fluency — naming the pattern is worth less than explaining why it fits here.
Strategy Pattern
A common RateLimiter interface with interchangeable algorithm implementations (Token Bucket, Sliding Window, ...) selected per route or client tier at runtime.
Decorator / Middleware Pattern
Rate limiting wraps the request handler as a composable middleware layer — stacked with auth and logging, without the handler ever knowing it exists.
Singleton / Factory Pattern
A RateLimiterFactory builds and caches limiter instances per config; the underlying store connection (e.g. a Redis pool) is typically a process-wide singleton.
Shared State Synchronization
Once you scale horizontally, bucket/window state must live in a shared store (Redis) or be reconciled across instances — this is where most interviews pivot to distributed design.