Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 84

Rate Limiting & Throttling: Algorithms & Implementations

Recommended resources

Hello! Welcome to the next lesson in our journey through backend engineering.

In our previous session, we explored the CAP theorem and the fundamental trade-offs distributed systems must make between consistency and availability. This underscored a core theme in system design: building for resilience and stability. A system that can't protect itself from being overwhelmed is neither consistent nor available.

Today, we'll build on that theme by looking at a critical protection mechanism: rate limiting and throttling. We will examine why it's an essential component of modern backend services and explore the different algorithms used to implement it. This is a topic that sits at the intersection of system design, scalability, and security.

By the end of this lesson, you'll be able to describe what rate limiting and throttling are, why they are used, and the mechanics of common algorithms like token bucket and sliding window.

What is Rate Limiting and Why Is It Necessary?

At its core, a rate limiter controls the amount of traffic a client or service can send or receive in a specified time frame. Think of it as a bouncer at the door of your application, ensuring that no single entity can monopolize the system's resources or cause a disruption.

The reasons for implementing a rate limiter are manifold and critical to a healthy backend architecture. To get a clear overview, let's start with a video that defines the concept and its purpose.

Design a Distributed Rate Limiter w/ a Ex-Meta Staff Engineer: System Design Breakdown

This video from Hello Interview kicks off with a great explanation of what a rate limiter is and why it's so important for protecting backend services.

Watch from 01:32 to 04:00. This section covers the definition of a rate limiter and its core functional requirements, such as identifying clients and enforcing configurable rules.

As the video explained, the primary goals of rate limiting are:

  • Preventing Abuse and DoS Attacks: It's your first line of defense against both malicious Denial of Service (DoS) attacks and unintentional overloads from misconfigured bots or clients.
  • Ensuring Fair Usage: It guarantees that a single "noisy neighbor" can't degrade the service quality for all other users.
  • Reducing Costs: For services that rely on paid third-party APIs, limiting calls is essential for cost management.
  • Preventing Server Overload: It protects your downstream components—databases, caches, and other microservices—from being overwhelmed by traffic spikes, which can prevent cascading failures.

Common Rate Limiting Algorithms

Now we get to the core of how rate limiters work. The choice of algorithm determines the trade-offs between accuracy, memory usage, and performance. Let's explore the most common ones.

Design A Rate Limiter - ByteByteGo

The article 'Design A Rate Limiter' from ByteByteGo provides an excellent, concise summary of the most popular algorithms. We'll use this as our primary reference for this section.

Read the section titled 'Algorithms for rate limiting'. It covers the Token bucket, Leaking bucket, Fixed window counter, Sliding window log, and Sliding window counter algorithms. As you read, focus on the pros and cons of each method.

To complement the reading, this video provides excellent animated explanations of the same algorithms, which can help solidify your understanding.

Five Rate Limiting Algorithms ~ Key Concepts in System Design

This video from Hello Byte offers clear visual walkthroughs for each algorithm, which can be very helpful for grasping the mechanics.

Watch from 01:16 to 16:43. Feel free to skip around to the algorithms you'd like to see visualized after reading about them. The contrast between Leaky Bucket (01:16) and Token Bucket (03:45) is particularly insightful.

Let's summarize the key algorithms and their trade-offs:

1. Fixed Window Counter

  • How it works: Time is divided into fixed windows (e.g., one minute). A counter tracks requests within the current window. When the window ends, the counter resets.
  • Pros: Simple to implement and memory-efficient.
  • Cons: Prone to the boundary effect. A burst of traffic at the edge of two windows can allow up to double the rate limit to pass through in a short period (e.g., making requests at 12:00:59 and 12:01:00).

2. Sliding Window Log

  • How it works: Stores a timestamp for every request in a log (e.g., a Redis sorted set). To check the rate, it counts the number of timestamps in the last time window.
  • Pros: Perfectly accurate. It completely solves the boundary effect of the fixed window algorithm.
  • Cons: High memory usage, as it needs to store a timestamp for every single request.

3. Sliding Window Counter

  • How it works: A hybrid approach that offers a good compromise. It tracks a counter for the current window and the previous window. It estimates the request count in the sliding window by taking a weighted sum of the two counters.
  • Pros: Smoother and more accurate than the fixed window counter, but far more memory-efficient than the sliding window log.
  • Cons: It's an approximation and assumes traffic is evenly distributed in the previous window.

4. Token Bucket

  • How it works: Each client has a "bucket" with a fixed capacity (burst capacity) that is refilled with "tokens" at a constant rate (sustained rate). Each request consumes one token. If the bucket is empty, the request is rejected.
  • Pros: Very popular and effective. It handles temporary bursts of traffic gracefully (up to the bucket's capacity) while enforcing a steady long-term rate. It's also memory-efficient, only needing to store the token count and last refill time per user.
  • Cons: Requires careful tuning of two parameters: the bucket size and the refill rate.
Token Bucket Algorithm Diagram
This diagram illustrates the Token Bucket algorithm. Tokens are added at a steady rate, and each request consumes one token. This allows the system to handle both a sustained load and short bursts of traffic.

The Token Bucket algorithm is arguably the most common choice for API rate limiting in production systems (used by Stripe, AWS, and many others) because it naturally accommodates the bursty nature of web traffic.


Designing a Distributed Rate Limiter

Knowing the algorithms is the first step. The next is understanding how to implement a rate limiter in a real-world, distributed backend system.

Since you are a full-stack developer, you know that modern backends are rarely single servers. They are distributed systems running multiple instances of the application logic. This presents a challenge: how do we maintain a single, consistent counter for a user when their requests might be handled by different servers?

The solution involves a few key design decisions:

  1. Placement: Where in the architecture does the rate limiter live?
  2. State Management: Where are the counters or token buckets stored?
  3. Concurrency: How do you handle race conditions when multiple requests for the same user arrive at once?

Let's turn back to the "Design a Distributed Rate Limiter" video, which walks through these exact problems.

Design a Distributed Rate Limiter w/ a Ex-Meta Staff Engineer: System Design Breakdown

This segment of the video covers the high-level design choices for building a scalable rate limiter.

Please watch the following sections: Where to place the rate limiter (14:33 - 21:11): Pay attention to the trade-offs of putting the limiter inside your microservice vs. at the API gateway. The Token Bucket algorithm (28:17 - 30:17): This recaps why Token Bucket is a great choice for this design. Implementation with Redis (30:17 - 35:43): This is the crucial part. Focus on how Redis is used as a centralized store and, most importantly, how Lua scripting solves the classic read-modify-write race condition.

As the video details, a robust, distributed rate limiter architecture typically looks like this:

Rate Limiter System Design Overview
A common architecture for a distributed rate limiter. The middleware (often at the API Gateway) intercepts requests, checks state in a centralized store like Redis, and either forwards the request to the API servers, drops it, or queues it.

Key Architectural Decisions

  • Placement at the Edge: The most common and effective place for a rate limiter is at the API Gateway or edge of your system. This allows you to reject excess traffic before it consumes any resources from your backend services.
  • Centralized State with Redis: To solve the distributed state problem, a fast, in-memory store like Redis is used. All application server instances communicate with the same Redis cluster to read and update rate limit counters, providing a global, shared state.
  • Handling Race Conditions: A critical issue in a concurrent environment is the read-modify-write race condition. Two servers could read a user's token count simultaneously, both decide the user has one token left, and both allow a request, incorrectly processing two requests when only one should have passed. This is solved by ensuring the entire operation is atomic. Redis achieves this through Lua scripting, which allows you to send a script that performs the read, calculation, and write in a single, uninterruptible operation.

Responding to the Client

When a request is rate-limited, it's not enough to just drop it. You must inform the client.

  • HTTP Status Code: The standard response is 429 Too Many Requests.
  • Informative Headers: Well-behaved APIs also include headers to help clients implement intelligent backoff strategies:
    • X-RateLimit-Limit: The total number of requests allowed in the window.
    • X-RateLimit-Remaining: The number of requests left in the current window.
    • X-RateLimit-Reset: A timestamp indicating when the limit will reset.

Conclusion

In this lesson, we've explored the crucial role of rate limiting in building stable and scalable backend systems. You've learned not just the "what" and "why" but also the "how," diving into the core algorithms and the architectural patterns needed to implement them in a distributed environment.

Key Takeaways:

  • Rate limiting is a critical mechanism for protecting services from overload, ensuring fair usage, and controlling costs.
  • There are several algorithms, each with trade-offs. The Token Bucket algorithm is a popular choice because it effectively handles both sustained and bursty traffic.
  • The main algorithms to know are Fixed Window Counter, Sliding Window Log, Sliding Window Counter, and Token Bucket.
  • In a distributed system, rate limiting requires a centralized data store (like Redis) to share state across server instances.
  • Handling the read-modify-write race condition with atomic operations (e.g., Redis Lua scripts) is essential for correctness in a concurrent environment.
  • When a client is rate-limited, the server should respond with a 429 status code and helpful headers like X-RateLimit-Limit and X-RateLimit-Reset.

Preview of the Next Lesson

We've now touched on several key aspects of system design: stateless services, data store trade-offs with CAP, and protecting services with rate limiting. These concepts apply regardless of how your backend is structured. However, the way we organize our services has massive implications for development speed, scalability, and operational complexity.

In the next lesson, we will compare monolithic and microservice architectures, identifying the primary trade-offs of each. We'll explore why the industry has seen a major shift towards microservices and discuss the significant challenges that come with this architectural style.