Redis Sliding Window Rate Limiting for High-Traffic APIs
Redis sliding window rate limiting provides precise API traffic control. Learn how to use ZSETs to build a distributed rate limiter that scales.
When my team first hit a wall with API abuse, we naively reached for a fixed-window counter. It worked until a spike at the edge of a window allowed a user to double their quota in milliseconds, forcing us to move toward a more robust Redis sliding window strategy.
If you’ve already explored the Token Bucket Algorithm for your services, you know that state management is the hardest part of distributed throttling. Unlike fixed windows, the sliding window algorithm provides a smooth, precise limit by tracking individual request timestamps.
Why Sliding Window Beats Fixed Windows
A fixed window resets at the top of the hour or minute. If your limit is 100 requests per minute, a user can fire 100 requests at 10:59:59 and another 100 at 11:00:01. That’s 200 requests in two seconds.
The sliding window solves this by treating time as a continuous flow. We store timestamps of each request in a Redis Sorted Set (ZSET). When a new request arrives, we discard all timestamps older than the current window duration and check if the remaining count is under our threshold.
Implementing the Distributed Rate Limiter with ZSETs
We use Redis ZSETs because they allow for atomic operations and range-based filtering. The score of each member is the timestamp, and the member itself is a unique identifier (like a UUID).
Here is the logic we use in production:
- ZREMRANGEBYSCORE: Remove all entries older than
now - window_size. - ZCARD: Check how many requests remain in the set.
- ZADD: If under the limit, add the current request timestamp.
- EXPIRE: Set a TTL on the key to clean up idle user data.
LUA-- Lua script for atomic execution local key = KEYS[1] local now = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local limit = tonumber(ARGV[3]) redis.call('ZREMRANGEBYSCORE', key, 0, now - window) local count = redis.call('ZCARD', key) if count < limit then redis.call('ZADD', key, now, now .. math.random()) redis.call('EXPIRE', key, math.ceil(window / 1000)) return 1 else return 0 end
Comparing Rate Limiting Patterns
Choosing the right approach depends on your memory constraints and accuracy requirements. While I prefer the sliding window for its precision, it does consume more memory than a simple incrementing counter.
| Algorithm | Accuracy | Memory Usage | Complexity |
|---|---|---|---|
| Fixed Window | Low | Very Low | Minimal |
| Token Bucket | Medium | Low | Moderate |
| Redis Sliding Window | High | High | Moderate |
For a deeper dive into how this stacks up against other methods, check out my breakdown in System Design Interview: Comparing Rate Limiting Algorithms.
Practical Considerations for High Traffic
When implementing this, keep an eye on your memory usage. If you have millions of unique users, storing every single request timestamp in a ZSET will bloat your Redis instance.
We mitigate this by:
- Short TTLs: Always set an expiration on the ZSET key.
- Sampling: For non-critical services, we use a sliding window log with a coarser resolution.
- Lua Scripting: Never perform the ZREMRANGE and ZADD as separate round-trips from your application. The network latency will kill your throughput. Keep it inside a single Lua script to ensure atomicity and speed.
If you're managing multi-tenant environments, you might also want to look at how we combine these techniques with Postgres in Postgres Rate Limiting and Redis Patterns for Multi-Tenant APIs.
Wrapping Up
The sliding window is my go-to when precision matters. It’s significantly more accurate than a fixed window and easier to reason about than a complex token bucket implementation.
Next time, I’d like to experiment with a Bloom filter-based approach for extreme scale, though I’m still worried about the false-positive rates in a distributed setup. Start with the ZSET approach—it’s battle-tested and likely all the API traffic control you'll need for your current scale.
Frequently Asked Questions
1. Does the ZSET approach support distributed environments? Yes, because Redis is a centralized data store. As long as your application instances share the same Redis cluster, the state remains consistent across all nodes.
2. How do I handle the performance impact of ZREMRANGEBYSCORE? ZSET operations are $O(\log N)$, where $N$ is the number of requests in the window. If your limit is set to a reasonable number (e.g., 500 requests per minute), the impact is negligible.
3. Is there a way to reduce memory usage for high-traffic users? If memory is a concern, consider using a "Sliding Window Counter" which uses two fixed-window counters and calculates a weighted average. It’s less accurate than the ZSET approach but uses significantly less memory.