Back to Blog
DatabasesJuly 9, 20264 min read

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.

Rediscachingperformancehigh-concurrencybackenddatabase

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:

  1. Check Cache: If data exists, return it.
  2. Try Lock: If missing, attempt to acquire a distributed lock using SET key:lock NX EX 5.
  3. Fetch Data: If the lock is acquired, query the database and update the cache.
  4. 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.

JAVASCRIPT
async 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.

StrategyComplexityBest For
Simple Cache-AsideLowLow-traffic, non-critical data
Request CoalescingMediumHot keys, expensive DB queries
Write-ThroughMediumReal-time data consistency
Write-BehindHighMassive 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.

Similar Posts