A stampede lock, and what the losers should do

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.