Redis Request Collapsing: Preventing Backend Overload
Stop cache stampedes with Redis request collapsing. Learn how to coalesce concurrent API requests, protect your database, and keep your backend snappy.
We’ve all been there: a popular product goes live, or a notification hits a million users at once. Your application gets hammered by a thousand identical requests for the same piece of data. If that data isn't in your cache, your database suddenly faces a "thundering herd" or "cache stampede." Your connection pool saturates, latency spikes, and your backend starts failing.
I recently spent about two days refactoring a high-traffic service that was collapsing under this exact scenario. We first tried simply increasing the database connection limit, but that just moved the bottleneck deeper into the storage layer. The real fix wasn't more hardware—it was ensuring that for any given "hot" key, only one request ever hits the origin database at a time.
The Request Coalescing Pattern
The core idea behind Redis request collapsing is simple: when a request arrives for a missing cache key, you "lock" the process. Subsequent concurrent requests for that same key see the lock, wait for the first request to finish, and then read the result from the cache.
Think of it like a single window at a busy ticket office. Instead of everyone rushing the window, one person goes up, and everyone else stands in a queue behind them. Once the first person gets their ticket, the clerk just hands the same info to everyone else in line.
Implementing Redis-Based Caching for Collapsing
To implement this, I use a combination of a SETNX (Set if Not Exists) lock and a short-lived cache entry. Here is the flow:
- Check Cache: If data exists, return it.
- Try Lock: If missing, attempt to acquire a distributed lock using
SET key:lock NX EX 5. - Fetch Data: If the lock is acquired, query the database and update the cache.
- Wait/Retry: If the lock isn't acquired, wait a few milliseconds and poll the cache again.
If you're dealing with heavy write pressure, you might want to look into how we handle write-buffer coalescing to further shield your primary storage.
A Practical Example
In a typical Node.js or Laravel environment, your implementation might look like this pseudo-code. I prefer using a Lua script to ensure the lock acquisition and check are atomic, preventing race conditions.
JAVASCRIPTasync function getCachedData(key) { const cached = await redis.get(key); if (cached) return JSON.parse(cached); // Try to acquire a lock for 5 seconds const lock = await redis.set(CE9178">`${key}:lock`, CE9178">'1', CE9178">'NX', CE9178">'EX', 5); if (lock) { const data = await db.query(CE9178">'SELECT...'); await redis.set(key, JSON.stringify(data), CE9178">'EX', 60); await redis.del(CE9178">`${key}:lock`); return data; } else { // Wait and retry await sleep(200); return getCachedData(key); } }
This approach is highly effective for preventing cache stampede scenarios. By forcing concurrent threads to wait, you turn a hundred database queries into one.
Comparison of Caching Approaches
When deciding how to handle high-concurrency data, it's helpful to see how these patterns stack up against each other.
| Strategy | Complexity | Best For |
|---|---|---|
| Simple Cache-Aside | Low | Low-traffic, non-critical data |
| Request Coalescing | Medium | Hot keys, expensive DB queries |
| Write-Through | Medium | Real-time data consistency |
| Write-Behind | High | Massive scale, eventual consistency |
Avoiding Common Pitfalls
The biggest mistake I see is setting a lock that never expires. If your backend worker crashes while holding the lock, you’ve effectively created a permanent outage for that specific resource. Always use an expiration (TTL) on your lock key, as shown in the EX 5 parameter above.
Also, be mindful of the "wait" time. If your database query takes 500ms, don't set your retry interval to 10ms. You'll just spam Redis with unnecessary read requests. Tuning this interval is part of the art of high-concurrency API optimization. If you're building a Laravel REST API Development project, you can often offload these patterns to background jobs or middleware to keep your controllers clean.
Why This Matters
If you're interested in deeper patterns, I've previously written about how to prevent cache stampedes at the database level. While that focuses on the DB, the logic remains the same: stop the duplicate work before it happens.
I'm still experimenting with using Redis Streams to handle these coalesced requests as a pub/sub queue, which might be even more efficient for extremely high-frequency keys. For now, the lock-and-retry pattern is the most robust way I've found to keep my infrastructure stable during traffic spikes. If you implement this, start with a conservative TTL on your locks and monitor your Redis CPU usage closely.