Hello! Welcome back to the course.
In our last lesson, we successfully connected your Spring Boot application to Redis and learned to use the fundamental GET, SET, INCR, and EXPIRE commands. We ended by identifying a critical flaw in the naive INCR + EXPIRE pattern for rate limiting: if your application crashes between these two commands, the counter key is left without a timeout, leading to a permanent block and a memory leak.
This lesson dives deep into the core reason behind that flaw and other, more subtle concurrency issues. We will explore why ensuring operations are "atomic" is not just an optimization but an absolute requirement for a correct and reliable distributed rate limiter.
Your learning outcome for this lesson:
By the end of this lesson, you will be able to explain why atomicity is critical for correctness in distributed rate limiters. This understanding will form the foundation for implementing robust, production-ready algorithms in the upcoming lessons.
1. The "Read-Modify-Write" Race Condition
The problem we saw with INCR and EXPIRE is just one type of race condition. A more common and insidious issue arises when multiple servers (or even multiple threads on one server) try to check and update a rate limit counter at the same time. This is known as the read-modify-write problem.
Imagine your rate limiter is deployed across several application servers, all sharing a central Redis instance. What happens when two requests for the same user arrive at the exact same moment on two different servers?
Let's watch a short, clear explanation of this scenario.
Rate Limiter System Design: Token Bucket, Leaky Bucket, Scaling
The channel ByteByteGo provides an excellent visual demonstration of this race condition. Pay close attention to how the final counter value becomes incorrect.
Watch the section from 06:26 to 06:48.
As the video shows, the sequence of events leads to a "lost update":
- Read: Server A reads the counter from Redis. Let's say the value is
3(and the limit is 5). - Read: At the same time, Server B reads the same counter. It also gets the value
3. - Modify: Server A checks the value, sees
3 < 5, and decides the request is allowed. It prepares to increment the counter to4. - Modify: Server B does the same. It also sees
3 < 5, allows the request, and prepares to increment to4. - Write: Server A writes the new value,
4, back to Redis. - Write: Server B writes its new value, also
4, back to Redis.
The result: Two requests were processed, but the counter only increased by one. The rate limiter is no longer accurate and will allow more traffic than configured.
This image provides another clear illustration of the problem and hints at the solution.

2. Why Basic Redis Transactions Are Not Enough
If you have experience with databases, your first thought might be to use a transaction. Redis provides the MULTI and EXEC commands to group multiple operations into a single transactional block.
Let's see how this would work for a token bucket algorithm:
- Read the user's current token count and last refill timestamp from Redis (
HMGET). - In your Java application, calculate how many new tokens to add based on the elapsed time.
- Start a transaction (
MULTI). - Update the new token count in Redis (
HSET). - Update the last refill timestamp (
HSET). - Execute the transaction (
EXEC).
This seems plausible, but it hides the same fundamental flaw. The problem is that the read operation happens before the transaction begins.
This article provides a fantastic explanation of this exact pitfall.
Design a Distributed Rate Limiter - Hello Interview
The article 'Design a Distributed Rate Limiter' from Hello Interview walks through a Redis-based token bucket implementation and clearly explains why a simple MULTI/EXEC transaction fails to prevent race conditions.
Read the section starting with 'Here's exactly how the Token Bucket algorithm works with Redis' down to the paragraph that begins 'Despite the MULTI/EXEC transaction...'. Focus on understanding why the read (HMGET) happening outside the transaction is the source of the race condition.
The key takeaway is that the atomic boundary of MULTI/EXEC only protects the write operations. By the time the transaction starts, another server could have already read the same initial data. Your application is making a decision based on potentially stale information. For a rate limiter to be correct, the entire read-modify-write cycle must be indivisible.
3. The Solution: Server-Side Logic with Lua
To make the entire cycle atomic, the logic—the "modify" part—must execute on the same server that stores the data, without interruption. In Redis, this is achieved using Lua scripts.
Redis can execute Lua scripts atomically. This means you can send a script that:
- Reads a value from a key.
- Performs calculations and applies logic (e.g., checks against a limit).
- Writes the new value back.
Redis guarantees that no other command will run while a script is executing. This effectively expands the atomic boundary to cover the full read-modify-write sequence, completely eliminating the race condition.
Let's see how this is presented as the definitive solution.
Design a Distributed Rate Limiter w/ a Ex-Meta Staff Engineer: System Design Breakdown
Let's revisit the 'Hello Interview' video, which introduces Lua scripting as the solution immediately after explaining the race condition.
Watch the segment from 34:34 to 35:43. The speaker explicitly states that the read and write operations must happen atomically and introduces Lua scripting as the way to achieve this in Redis.
This approach not only ensures correctness but also improves performance by reducing the number of network round-trips between your application and Redis.
This next resource frames the transition from application-side logic to server-side scripts in the context of a Spring Boot project, which is highly relevant to our goal.
Efficient Rate Limiting with Spring Boot and Redis
This practical guide by Ercan Sormaz discusses building a rate limiter with Spring Boot and Redis. It provides clear reasons for moving from Java-based Redis operations to Lua scripts.
Please read the short sections titled 'From Java Logic to Lua Scripts', 'Why Lua Scripts Matter', and 'Lessons Learned'. Notice how the author emphasizes atomicity, consistency, and performance as the primary drivers for using Lua.
As the article concludes, "Atomicity matters." Without it, a distributed rate limiter is fundamentally unreliable under the exact conditions it's designed to handle: high, concurrent traffic.
Conclusion
In this lesson, we established a crucial principle for our project. Simply using a fast, centralized data store like Redis is not enough. The logic for checking and updating rate limits must be executed atomically to prevent race conditions that would make the limiter inaccurate.
Key Takeaways:
- Distributed rate limiters are vulnerable to a read-modify-write race condition, where concurrent requests can lead to lost updates and incorrect counting.
- This means the rate limiter will fail to enforce its configured limits accurately, especially under heavy load.
- Basic Redis transactions using
MULTI/EXECare insufficient because the initial read of the counter state occurs outside the atomic transaction block. - The correct solution is to encapsulate the entire read-modify-write cycle into a single, indivisible operation that runs on the Redis server itself. In Redis, this is achieved using Lua scripting.
Next Steps
You now understand why we need Lua. In our next lesson, "Write a basic Lua script to execute multiple Redis commands atomically," we will move from theory to practice. You'll learn the essential syntax of the Lua language and write your first script to finally solve the INCR + EXPIRE race condition we've been discussing. This will be a major step toward building our first robust, distributed rate limiting algorithm.