Introduction
Welcome to our new module, "Service Resilience and Fault Tolerance." In the previous module on Core Backend Architecture, we focused on designing stable and predictable APIs, culminating in our lesson on idempotency. You learned how to use idempotency keys to protect your system from the side effects of unintentional duplicate requests, ensuring that a client's retry doesn't cause a double payment.
Today, we shift our focus from protecting against accidental retries to protecting your system from being overwhelmed by too many requests, whether they're from a misbehaving client, a malicious actor, or simply unexpected viral traffic. This is the domain of rate limiting.
Your learning outcome for this lesson is to design a rate-limiting system by comparing token bucket vs. leaky bucket algorithms for protecting services from overload. This is a fundamental building block for any robust backend system and a very common topic in senior backend interviews.
Framing the Problem: What is a Rate Limiter?
A rate limiter is a gatekeeper for your services. It controls the amount of traffic a client can send to an API in a given time frame. For an interview, you must start by clarifying the requirements before jumping to solutions.
Let's watch a segment from a mock system design interview that does an excellent job of breaking down the problem.
Design a Distributed Rate Limiter w/ a Ex-Meta Staff Engineer: System Design Breakdown
The video 'Design a Distributed Rate Limiter' from Hello Interview provides a clear, interview-oriented framework. It starts by defining what a rate limiter is and then systematically outlines the functional and non-functional requirements.
Watch from 01:41 to 11:46. As you watch, focus on how the speaker frames the problem: Functional Requirements: What should the system do? (e.g., identify clients, apply configurable rules, return proper errors). Scale Estimation: How is the problem scoped? (e.g., 1 million requests per second). Non-Functional Requirements: What qualities must the system have? Pay close attention to how he quantifies them (e.g., low latency under 10ms, scalability to 1M RPS, and high availability over consistency).
As the video demonstrates, a strong interview answer begins with structure. Here’s a summary of the key requirements we'll design for:
- Functional:
- Identify clients by IP, user ID, or API key.
- Limit requests based on configurable rules (e.g., 100 requests/min/user).
- Return an
HTTP 429 Too Many Requestserror when the limit is exceeded.
- Non-Functional:
- Low Latency: The check must be fast (e.g., <10ms) to avoid slowing down valid requests.
- High Availability: The system should favor being online over being perfectly consistent (an AP system in CAP theorem terms). A few extra requests getting through is better than the whole system going down.
- Scalability: Must handle a high volume of traffic (e.g., 1 million requests per second).
- Distributed: Must work across multiple instances of our service.
Architectural Placement: Where Does the Rate Limiter Live?
Before diving into algorithms, we must decide where to place our rate limiter. This architectural choice has significant implications for latency, complexity, and effectiveness.
Design a Distributed Rate Limiter w/ a Ex-Meta Staff Engineer: System Design Breakdown
The same video discusses the trade-offs of different placements. This is a critical architectural decision point.
Please watch from 15:53 to 21:17. Note the pros and cons of the three main options: In-Process (Bad): Why does this fail in a distributed environment? Dedicated Service (Good): What is the main drawback of this approach? API Gateway/Edge (Great): Why is this the most common and effective solution for our use case?
To summarize the trade-offs:
| Placement | Pros | Cons |
|---|---|---|
| In-Process | Very fast (no network calls) | No global state; ineffective in a distributed system. A user could bypass limits by hitting different servers. |
| Dedicated Service | Centralized logic, can use rich business context | Adds a network hop (latency) to every request. Becomes a single point of failure. |
| API Gateway (Edge) | Centralized, protects all downstream services. No extra latency for application logic. | Limited context (only what's in the HTTP request). Depends on an external store (like Redis) for state. |
For our design, we'll adopt the standard production pattern: implementing the rate limiter at the API Gateway. This provides centralized protection efficiently without adding latency to every service-to-service call.

Core Algorithms: Token Bucket vs. Leaky Bucket
Now we arrive at the core of the problem: the algorithm that decides whether to allow or reject a request. While several algorithms exist, we'll focus on the two most important ones for an interview setting: Token Bucket and Leaky Bucket.
1. Token Bucket
Imagine each user has a bucket of a fixed size. This bucket is continuously refilled with "tokens" at a steady rate.
- Each incoming request attempts to take one token from the bucket.
- If a token is available, the request is allowed, and the token count decreases.
- If the bucket is empty, the request is rejected.
This algorithm is defined by two parameters:
- Bucket Size: The maximum number of tokens the bucket can hold. This defines the maximum burst of requests allowed.
- Refill Rate: The rate at which tokens are added to the bucket (e.g., 10 tokens per second). This defines the sustained average rate.
Design a Distributed Rate Limiter w/ a Ex-Meta Staff Engineer: System Design Breakdown
The Hello Interview video provides an excellent conceptual and implementation overview of the Token Bucket algorithm.
Watch from 28:09 to 30:17. Focus on how the token bucket elegantly handles both bursts and sustained load.
The key advantage of the token bucket is its flexibility. It allows for legitimate bursts of traffic (e.g., a web app making several API calls on page load) while still enforcing a long-term average rate.
2. Leaky Bucket
Now, let's look at a different model. Imagine a bucket with a hole in the bottom.
- Incoming requests are like water being poured into the bucket. The bucket represents a queue (First-In, First-Out).
- Requests "leak" out of the bottom at a fixed, constant rate to be processed.
- If requests arrive faster than they are processed, the bucket fills up.
- If the bucket is full when a new request arrives, it "overflows" and the request is discarded.
Rate Limiting system design | TOKEN BUCKET, Leaky Bucket, Sliding Logs
The video 'Rate Limiting system design' by Tech Dummies gives a clear and simple explanation of the Leaky Bucket algorithm.
Watch from 10:41 to 12:52. Notice how this algorithm's primary goal is to smooth out traffic into a constant stream.
The primary characteristic of the leaky bucket is that it enforces a constant output rate, regardless of how bursty the input is.
Head-to-Head Comparison
This visual and table summarize the critical differences, which are prime material for an interview trade-off discussion.
| Feature | Token Bucket | Leaky Bucket |
|---|---|---|
| Primary Goal | Allow bursts while maintaining an average rate. | Enforce a strict, constant output rate. |
| Key Parameters | Bucket Size (burst), Refill Rate (average) | Bucket Size (queue size), Outflow Rate (constant) |
| Traffic Output | Bursty | Smooth and constant |
| Implementation | Typically a counter (tokens, last_refill_time) | Typically a queue (FIFO) |
| Common Use Case | Most public APIs (e.g., GitHub, Stripe) | Controlling traffic to a downstream service that cannot handle bursts. |
| Resource Usage | Memory-efficient (just two variables per user). | Can be memory-intensive if the queue holds large requests. |
For our general-purpose API, the Token Bucket algorithm is the superior choice. It provides the flexibility to handle legitimate client-side bursts while still protecting our backend, which is why it's the de-facto standard for many public-facing APIs.
Implementation in a Distributed System
Choosing the token bucket algorithm is just the first step. For a senior role, you must explain how to implement it in a distributed, scalable, and fault-tolerant way.
This introduces two major challenges:
- Shared State: Where do we store the token buckets for millions of users so all our gateway instances see the same data?
- Race Conditions: What happens if two requests for the same user arrive at two different servers at the exact same time?
The solution to both is to use a fast, centralized, in-memory datastore like Redis.
Design a Distributed Rate Limiter w/ a Ex-Meta Staff Engineer: System Design Breakdown
Let's return to the 'Hello Interview' video, which brilliantly explains how to use Redis to implement a distributed token bucket, including how to solve the critical race condition problem.
This is a dense but crucial segment. Watch from 30:17 to 49:32, breaking it down as follows: Using Redis for State (30:17 - 33:30): Understand the basic flow of fetching, calculating, and updating token counts in Redis. The Race Condition (33:30 - 35:43): Pay close attention to the read-modify-write problem and why a simple transaction is not enough. The Solution: Lua Scripting (35:00 - 35:43): Recognize that using a Redis Lua script makes the entire read-calculate-update cycle atomic, preventing race conditions. Scaling with Sharding (39:29 - 42:54): How do we handle more traffic than one Redis instance can manage? Fault Tolerance with Replicas (42:54 - 47:30): What happens if a Redis node fails? Understand the fail-open vs. fail-close trade-off and the role of replicas.
Production-Ready Implementation Summary:
-
Centralized State: Use a Redis cluster to store the token bucket state (
token_count,last_refill_timestamp) for each client key (e.g.,user_id:123). Redis is chosen for its low latency. -
Atomicity: To prevent race conditions where two servers read the old token count simultaneously, perform the entire check-and-update logic inside a Redis Lua script. This guarantees the operation is atomic. A server sends a single command to Redis: "For this user, calculate new tokens, check if one is available, decrement if so, and tell me the result."
-
Scalability: A single Redis instance won't scale to millions of RPS. Shard the data across multiple Redis nodes. A common strategy is to use consistent hashing on the client key (
user_idorip_address) to determine which Redis shard holds that client's bucket. Redis Cluster handles this for you automatically. -
Fault Tolerance: To prevent a single Redis node failure from taking down the system, use replication. Each primary shard has one or more replica shards. If a primary fails, a replica is promoted. This is an Available-Partition Tolerant (AP) system, prioritizing availability. A few requests might be miscounted during a failover, but the system remains online.
Closing the Loop: Responding to the Client
Finally, when a request is rejected, we must inform the client properly. This is not just about returning an error; it's about providing actionable feedback.
Design a Distributed Rate Limiter w/ a Ex-Meta Staff Engineer: System Design Breakdown
The final piece is handling the response for a rate-limited request.
Watch from 35:43 to 38:41. Note the standard HTTP status code and the conventional headers used to provide context to the client.
When a client is rate-limited:
- Fail Fast: Immediately reject the request. Do not queue it, as this adds complexity and can lead to cascading failures.
- Status Code: Return
429 Too Many Requests. - Response Headers: Include informative headers to help the client implement intelligent retry logic:
X-RateLimit-Limit: The total request limit for the window.X-RateLimit-Remaining: The number of requests remaining in the window.X-RateLimit-Reset: A Unix timestamp indicating when the limit will reset.Retry-After: The number of seconds the client should wait before retrying.
Conclusion
In this lesson, you've designed a complete, distributed, and robust rate-limiting system ready for a senior-level interview discussion.
Key Takeaways:
- Rate limiting is a critical mechanism for ensuring API stability, security, and fairness.
- The Token Bucket algorithm is generally preferred for APIs as it allows for legitimate bursts while enforcing an average rate. Leaky Bucket is better for smoothing traffic into a constant stream.
- A production-ready rate limiter must be distributed. This requires a centralized, low-latency store like Redis.
- Handling race conditions with atomic operations (like Redis Lua scripts) is non-negotiable for correctness.
- Scalability is achieved through sharding, and fault tolerance is achieved through replication, embracing a "fail-open" or "fail-close" strategy depending on business needs.
Preview of the Next Lesson:
Our rate limiter now correctly tells clients to "try again later." But how should a client act on that? Simply retrying immediately could prolong the overload. In our next lesson, we'll look at the client-side of resilience by learning how to implement a retry strategy with exponential backoff and jitter, a pattern that prevents clients from causing a "thundering herd" problem.