The simpler stampede fix is a lock, and it converts a burst of computation into a burst of waiting.
$lock = Cache::lock("recompute:{$key}", 10);
if ($lock->get()) {
try { return $this->recomputeAndStore($key); }
finally { $lock->release(); }
}
// and the decision for everybody else:
return $this->stale($key) // serve stale, if there is one
?? $this->waitFor($lock, 2) // or wait, briefly
?? throw new Unavailable(); // or fail fast
Serving stale is nearly always the right branch and requires keeping the old value past its logical expiry, which means two TTLs per entry. Waiting is acceptable for a short computation and is a queue of held connections for a long one — which turns a cache miss into a capacity problem, and is the failure this was meant to prevent.