Repository navigation
Improve handling of cache stampeding / thundering herd #60781
Replies: 6 comments 4 replies
|
I like that this is proposed as an opt-in feature. One question I have is whether the behavior would be consistent across different cache drivers. Drivers like Redis can support atomic operations and distributed locks, while file or database caches have different capabilities. Do you envision this as a driver-agnostic feature implemented entirely in Laravel, or would each cache driver be expected to optimize the behavior differently? |
|
I frequently need a beautiful solution to cache stampedes. I did submit a pull request myself but was denied. |
|
This landed in 11.x as $value = Cache::flexible('key', [300, 3600], fn () => expensiveComputation());Two TTLs: fresh for 300 seconds, usable for an hour. Inside the fresh window it returns immediately. Past it, the caller still gets the stale value and the recompute is deferred, so nobody waits and the herd never forms. if (($created + $this->getSeconds($ttl[0])) > Carbon::now()->getTimestamp()) {
return $value;
}
$refresh = function () use ($key, $ttl, $callback, $lock, $created) {
$this->store->lock(
"illuminate:cache:flexible:lock:{$this->itemKey($key)}",
$lock['seconds'] ?? 0,
$lock['owner'] ?? null,
)->get(function () use ($key, $callback, $created, $ttl) {
if ($created !== $this->get(self::FLEXIBLE_CREATED_KEY_PREFIX.$key)) {
return;
}
...Two guards there, not one. The lock keeps concurrent refreshers out, and the On the driver question upthread: it is driver-agnostic in the sense that matters, because locking is not Redis-only. Compared with the probabilistic early-miss idea, the tradeoff is that a lottery still makes some unlucky request pay the full recompute cost while everyone else is served from cache, and you tune a probability to control how often that happens. |
|
The stale window for Cache::flexible has a cache lock around 'expensiveComputation', but a cold/fully expired cache does not. |
|
Laravel's Cache::flexible() handles stale-while-revalidate but doesn't lock, so concurrent misses can still hammer the backend. Fix: add locking inside flexible() (via Cache::lock()) so only one process regenerates while others get stale data or block briefly — making stampede protection built-in rather than requiring manual Cache::lock() wrapping. |
|
This discussion, particularly the missing bulk equivalent of It adds I'm the author. It's MIT-licensed and still in beta, so the API may change. Before making a stable release, I'd like to find out whether it's useful in other applications and what I've overlooked. Would this fit the bulk-cache workloads you had in mind? Examples where it falls short or adds unnecessary complexity would be useful too. If anyone tries it locally or in staging, I'd appreciate hearing how it goes. Feedback on the API alone would also help. |
Uh oh!
There was an error while loading. Please reload this page.
Currently Laravel does not really have anything effective against cache stampeding / thundering herd. When cache entry expires, we would have multiple Laravel processes trying to repopulate the cache entry. This could be a problem.
One way to deal with this is an opt-in lottery-based "read failures". By a low-probability lottery (opt-in), the Cache layer pretends that a cached value doesn't exist when the entry still has not expired yet. This allows a limited number of Laravel processes to update the cached entry before it expires. This should mitigate the above-mentioned stampede problem.
Perhaps Laravel can introduce this feature?
All reactions