Redis Caching Strategies That Prevent Thundering Herds and Cache Stampedes
Protect your primary database from catastrophic failure during traffic spikes. Learn how to implement Probabilistic Early Expiration (XFetch), mutex locking, and tiered in-memory caching.
1. The Nightmare of the Cache Stampede
Imagine this scenario: your e-commerce storefront or SaaS homepage caches a complex, aggregated query (such as 'trending products' or 'top public templates') in Redis with a 60-minute Time-To-Live (TTL). The query takes 800ms to compute on your primary PostgreSQL database.
At exactly 12:00:00 PM, the cache key expires. Over the next 500 milliseconds, 3,000 concurrent user requests arrive. Finding the cache key empty, every single request simultaneously fires the heavy 800ms query against PostgreSQL.
This is the Thundering Herd (or Cache Stampede). PostgreSQL's CPU instantly hits 100%, query queues fill up, database connection pools exhaust, and the entire platform crashes. Adding more web servers only worsens the stampede.
2. The Solution: Probabilistic Early Expiration (XFetch Algorithm)
The most elegant mathematical solution to the cache stampede problem is the XFetch algorithm (developed by Vattani et al.).
Instead of waiting for the key to expire completely, as the remaining TTL diminishes, requests have an increasing probabilistic chance of recomputing the value in the background while still serving the current cached value to other users.
The formula calculates whether to recompute based on: remaining TTL, the time it took to compute the value previously (`delta`), and a tuning parameter (`beta`):
`-delta * beta * ln(random()) > remaining_ttl`
3. Implementation: Production XFetch in TypeScript
Here is a complete, production-ready implementation of the XFetch algorithm using Node.js and Redis:
export interface CachedRecord<T> {
value: T;
delta: number; // Duration in ms it took to compute the value
expiresAt: number; // Unix timestamp in ms
}
export class ResilientCache {
constructor(private redis: any) {}
async getOrCompute<T>(
key: string,
ttlSeconds: number,
computeFn: () => Promise<T>,
beta: number = 1.0
): Promise<T> {
const raw = await this.redis.get(key);
let record: CachedRecord<T> | null = raw ? JSON.parse(raw) : null;
const now = Date.now();
// Check XFetch probabilistic condition:
// If record is missing, OR probabilistic threshold is met
const shouldRecompute =
!record ||
now >= record.expiresAt ||
-record.delta * beta * Math.log(Math.random()) > (record.expiresAt - now);
if (shouldRecompute) {
const startTime = Date.now();
const freshValue = await computeFn();
const delta = Date.now() - startTime;
const newRecord: CachedRecord<T> = {
value: freshValue,
delta,
expiresAt: now + ttlSeconds * 1000,
};
// Save with safety buffer TTL in Redis
await this.redis.set(
key,
JSON.stringify(newRecord),
"EX",
ttlSeconds + 60
);
return freshValue;
}
return record.value;
}
}4. Distributed Mutex Locking (Redlock)
For expensive operations where you cannot tolerate even a probabilistic duplicate run (e.g. generating an expensive daily PDF financial report), use a distributed mutex lock in Redis.
When a cache miss occurs, the first process acquires a non-blocking lock with a short lease: `SET key:lock uuid NX EX 10`. Only the lock holder queries the database; other concurrent requests wait 50ms and retry fetching the newly cached value.
This ensures that exactly one database query executes regardless of how many thousands of requests arrive concurrently.
What is the difference between Cache-Aside and Write-Through caching?
Cache-Aside checks the cache first and queries the database on miss. Write-Through updates both the database and the cache synchronously on every write.
How does Redis handle out-of-memory situations?
When memory limits are reached, Redis evicts keys according to its `maxmemory-policy`. For web caching, `allkeys-lru` or `volatile-lru` is recommended.
Can Redis replace a primary database?
While Redis supports persistence (AOF and RDB), it lacks relational integrity constraints, ACID multi-table transactions, and complex indexing. Use Redis as a high-speed cache alongside PostgreSQL.