Imagine your application has a popular API endpoint.
You cache its response in Redis for 5 minutes to avoid hitting the database repeatedly.
Everything works perfectly.
Until the cache expires.
Suddenly, 10,000 requests arrive at the same time. They all find an empty cache and start querying your database.
Congratulations. Your cache just caused a database traffic spike.
This problem is called a Cache Stampede, also known as the Thundering Herd.
The Usual Solution: Distributed Locks
A common solution is to use a distributed lock.
When the cache expires:
- The first request acquires a lock.
- It fetches fresh data from the database.
- Other requests wait or use stale data.
- The cache gets updated.
It works, but now you're managing locks, timeouts, and failure cases.
What if we could refresh the cache before it expires?
Meet XFetch
XFetch is a clever algorithm based on probabilistic early expiration.
Instead of waiting until the cache expires, it randomly decides whether to refresh it early.
Here's the interesting part:
- When the cache is fresh, refreshing is unlikely.
- As expiration approaches, refreshing becomes more likely.
- Slower database queries encourage earlier refreshes.
Think of it like replacing a phone battery before it dies instead of waiting for it to shut down.
No distributed lock is required to make the refresh decision.
The Tiny Formula Behind It
now - β × delta × ln(random()) >= expiry
Looks complicated? It's actually simple.
-
now— Current time -
expiry— When the cache expires -
delta— How long the last refresh took -
random()— A random number between 0 and 1 -
β— Controls how aggressively we refresh
The closer we get to expiration, the more likely the condition becomes true.
That's the core idea.
A Simple JavaScript Example
Here's the early-expiration decision in JavaScript:
function shouldRefresh(expiry, delta, beta = 1) {
const random = Math.max(Math.random(), Number.MIN_VALUE);
return (
Date.now() - beta * delta * Math.log(random) >= expiry
);
}
That's it!
Let's say:
const expiry = Date.now() + 5000;
const delta = 200;
console.log(shouldRefresh(expiry, delta));
The function returns true or false.
If it returns true, the application can refresh the cached value early.
The important detail is that delta and expiry use the same time unit. Here, both are in milliseconds.
But There's a Catch
XFetch is smart, but it isn't magic.
Multiple requests can still decide to refresh simultaneously. It reduces the risk of a stampede; it doesn't guarantee exactly one refresh.
Also, if you delete the old cache entry before refreshing, other requests can still experience cache misses.
A practical implementation should keep serving the existing value while refreshing, where stale data is acceptable.
For stronger protection, you can combine early expiration with a single-flight mechanism or a short-lived refresh lock.
When Should You Use It?
XFetch is particularly useful for:
- Popular API responses
- Homepage feeds
- Trending products
- Expensive database queries
- Frequently accessed cached data
For applications where a little stale data is acceptable, probabilistic early refresh can be a simple way to reduce sudden database load.
For strict freshness requirements, consider additional coordination.
Final Thoughts
Most caching strategies ask:
"Has the cache expired?"
XFetch asks a smarter question:
"Should we refresh it before it expires?"
That small change can help prevent thousands of requests from rushing to your database at once.
Sometimes, a little randomness is exactly what your system needs.
Top comments (4)
Nice to see XFetch getting attention. Probabilistic early refresh is one of those fixes that feels too simple to work until you look at the math. Worth placing it next to the alternatives: a lock serializes the refresh but adds a failure mode, stale-while-revalidate serves old data while one worker refreshes, and XFetch spreads refreshes out so the expiry cliff never forms. Cache stampede is also a favorite interview follow-up. When I cover caching at DesignGurus, "what happens when this key expires under load" is where many otherwise solid designs fall apart.
Caching is easy to explain until you start thinking about what happens under heavy traffic. Each approach has its own trade-offs, and knowing when to use locks, stale-while-revalidate, or XFetch is what really matters. And I agree, cache expiration is one of those small details that can turn into a big production issue. Thanks.
the part that bites is delta under load. the slower the refresh gets, the wider the window where every new request rolls the same dice, so you can get duplicates for the whole duration of one slow query, not just at the expiry moment. that's where I end up adding the single-flight anyway.
Exactly! XFetch helps decide when to refresh, but it doesn't prevent multiple requests from refreshing at the same time. When queries get slow, those overlapping refreshes can still put pressure on the database. Combining XFetch with single-flight makes more sense in production. XFetch helps refresh early, while single-flight prevents duplicate work for the same cache key. Best of both approaches.