An expensive cached value expires under load and every concurrent request misses simultaneously, so forty workers run the same four-second query at once.
$value = Cache::get($key);
if ($value === null) {
$lock = Cache::lock("{$key}:build", 30);
if ($lock->get()) {
try {
$value = $this->build();
Cache::put($key, $value, 300);
Cache::put("{$key}:stale", $value, 1800);
} finally {
$lock->release();
}
} else {
$value = Cache::get("{$key}:stale") ?? $this->build();
}
}
What the losers do is the design decision: serving a stale copy is best where staleness is acceptable, waiting is acceptable where it is not, and building anyway is the safety valve that keeps a lock failure from becoming a total failure. The finally is not optional — a build that throws while holding the lock blocks everything until it expires. The second cache entry with a longer expiry is what makes the stale path possible at all.