When an interviewer asks you to design a rate limiter, they are looking for three things: a clear understanding of the problem space, the ability to pick the right algorithm, and a sensible distributed architecture. Below is a walkthrough you can follow in real time, keeping the conversation focused and grounded in concrete choices.
1. Clarify the Scope
Start by asking a few clarifying questions. This shows you care about trade‑offs and avoids building a solution for the wrong problem.
- What is being limited? API calls, messages, login attempts, or something else?
- What granularity is needed? Per user, per IP, per API key, or per service?
- What is the acceptable burst size? Do callers need to send a quick burst of requests, or must the traffic be smooth?
- Latency tolerance? Does the limiter need to respond in sub‑millisecond time, or is a few milliseconds acceptable?
- Persistence requirements? Should limits survive a restart, or is an in‑memory reset fine?
These questions let you decide between a simple in‑process limiter and a fully distributed system.
2. Functional & Non‑Functional Requirements
Functional
- Allow a configurable number of requests per time window (e.g., N requests per T seconds).
- Support multiple keys (user ID, IP, API key) simultaneously.
- Expose an API for checking and updating limits.
- Provide a way to reset or adjust limits at runtime.
Non‑Functional
- Low latency – the check should be fast enough not to become a bottleneck.
- Scalability – the system should handle growth in request volume and number of keys.
- Fault tolerance – a node failure must not cause a global outage.
- Consistency – the limiter must not allow a user to exceed the defined quota, even under heavy contention.
3. Core Entities & API
Entities
| Entity | Responsibility |
|---|---|
Limiter | Holds configuration (limit, window, burst) and delegates to an algorithm implementation. |
TokenBucket / LeakyBucket | Implements the core rate‑limiting logic. |
CounterStore | Persists per‑key counters (in‑memory cache, Redis, or a specialized KV store). |
KeyResolver | Maps a request to a limiting key (e.g., extracts user ID). |
API Sketch (pseudo‑code)
// Returns true if the request is allowed, false otherwise.
func Allow(key string) (bool, error)
// Optional: returns remaining quota and time until reset.
func Stats(key string) (remaining int, resetIn time.Duration, err error)
// Admin: change limit for a specific key.
func SetLimit(key string, limit int, window time.Duration) error
The API is deliberately small; interviewers often probe how you would extend it later.
4. High‑Level Architecture
Below is a text diagram that you can draw on a whiteboard.
+-------------------+ +-------------------+ +-------------------+
| Client / API | -----> | Rate‑Limiter | -----> | Counter Store |
| (Gateway, Edge) | | Service (stateless) | (Redis / Memcached) |
+-------------------+ +-------------------+ +-------------------+
| |
| Consistent Hashing (key) |
+-------------------------------+
Key points
- The limiter service is stateless; it only reads/writes counters.
- Consistent hashing distributes keys across multiple service instances, limiting hot‑spot risk.
- The counter store should support atomic increment/decrement with TTL (time‑to‑live) semantics.
5. Deep Dive: Token Bucket vs Leaky Bucket
Token Bucket
- How it works: Tokens are added to a bucket at a fixed rate. A request consumes a token; if none are available, the request is rejected.
- Strengths: Allows bursts up to the bucket size; easy to implement with a simple counter and timestamp.
- Weaknesses: Requires tracking both token count and last refill time, which can complicate distributed consistency.
Leaky Bucket
- How it works: Requests enter a queue that drains at a constant rate. If the queue is full, excess requests are dropped.
- Strengths: Guarantees a smooth output rate; easier to reason about when strict pacing is needed.
- Weaknesses: Does not naturally support bursts; the queue can become a latency source.
Comparison Table
| Feature | Token Bucket | Leaky Bucket |
|---|---|---|
| Burst support | Yes (bucket size) | No (fixed rate) |
| Implementation complexity | Medium (refill logic) | Low (simple queue) |
| Typical use case | API rate limits, user‑level quotas | Traffic shaping, bandwidth throttling |
| Distributed consistency | Requires careful timestamp sync | Simpler with atomic decrement |
When interviewers ask which algorithm you prefer, answer based on the burst requirement you uncovered earlier.
6. Distributed Considerations
a. Counter Store Choice
- Redis (or compatible KV store) is a common choice because it offers atomic
INCRwith expiration, which maps directly to the token‑bucket refill. - In‑memory caches (e.g., Memcached) can be used for ultra‑low latency but lack persistence; suitable if loss of counters on restart is acceptable.
b. Consistency & Race Conditions
- Use Lua scripts (in Redis) to perform read‑modify‑write atomically. This eliminates the classic “check‑then‑set” race.
- For very high QPS, sharding keys across multiple Redis instances reduces contention.
c. Scaling the Service Layer
- Deploy the limiter as a stateless microservice behind a load balancer.
- Leverage horizontal autoscaling based on CPU or request latency metrics.
- Keep the service lightweight; most work is a single KV store round‑trip.
7. Trade‑offs & Follow‑up Questions
Interviewers love to explore the edges of your design. Be ready to discuss:
- What if the limit needs to be dynamic per user? Explain how
SetLimitcan be stored alongside the counter and fetched on each request. - How would you handle a sudden traffic spike? Talk about burst capacity, fallback to a secondary limiter, or queuing at the edge.
- What about multi‑region deployments? Suggest using a geo‑distributed KV store with eventual consistency, or routing users to the nearest region and accepting a small over‑limit risk.
- Can you provide metrics for monitoring? Mention request‑allowed vs. request‑rejected rates, latency histograms, and alerting on error spikes.
- How would you test the limiter? Unit tests for token refill logic, integration tests against a real KV store, and load‑testing with a traffic generator.
8. Where Call Assistant Helps
Practicing this walkthrough aloud is often the hardest part. Using Call Assistant, you can rehearse your explanation while it captures the flow and suggests concise phrasing. It also helps you stay on track when interviewers throw follow‑up questions, ensuring you keep the story anchored to your resume.
How to practice this
- Write the API and sketch the diagram on paper, then explain each component out loud for 2‑3 minutes.
- Simulate follow‑up questions (e.g., “What if the limit changes daily?”) and answer them, refining your trade‑off analysis each time.
- Record a short session with Call Assistant and review the transcript for filler words or unclear phrasing, then iterate.
FAQ
- What is the simplest way to implement a rate limiter for a single server? Use an in‑memory token bucket that stores the last refill timestamp and remaining tokens per key; update them on each request.
- When should I prefer a leaky bucket over a token bucket? Choose leaky bucket when you need a smooth, constant output rate and bursts are not required, such as shaping outbound bandwidth.
- How do I ensure the limiter stays fast under high QPS? Keep the service stateless, use a fast KV store with atomic operations, and shard keys across multiple instances to avoid contention.
- Can a rate limiter be versioned without downtime? Yes; expose the limit configuration via a separate admin API and reload it dynamically, allowing you to roll out new limits gradually.
Frequently asked questions
What is the simplest way to implement a rate limiter for a single server?
Use an in‑memory token bucket that tracks the last refill time and remaining tokens per key, updating them on each request.
When should I prefer a leaky bucket over a token bucket?
Pick leaky bucket when you need a smooth, constant output rate and bursts are not required, such as for traffic shaping.
How do I ensure the limiter stays fast under high QPS?
Make the service stateless, rely on a fast key‑value store with atomic increments, and shard keys across multiple instances to reduce contention.
Can a rate limiter be versioned without downtime?
Yes; expose limit settings through an admin API and reload them dynamically, allowing gradual rollout of new limits.
#system design#rate limiter#distributed systems#api design#interview prep#a rate limiter