A cache expiry synchronizes callers unless refresh ownership is controlled. Stale-while-revalidate trades bounded freshness for stable origin load.
Stale-While-Revalidate Boundary
Cache expiry must not synchronize callers into an origin outage.
const cached = await cache.get(key)
if (cached?.freshUntil > Date.now()) return cached.value
if (cached?.staleUntil > Date.now()) {
if (await lease.tryAcquire(key, 10_000)) {
void refresh(key).finally(() => lease.release(key))
}
return cached.value
}
// A miss also needs shared single-flight; do not fan out origin loads.
return lease.runExclusive(key, () => loadAndCache(key))Invariant: One refresh occurs per hot key while callers receive bounded-stale data.
Use when: A hot key expiring could stampede the database.
Why this boundary matters
A hot-key expiry can release thousands of identical origin reads. One refresh owner contains that load while bounded-stale data preserves service.
Failure policy
| Boundary | Action |
|---|---|
| Fresh value | Serve immediately |
| Stale but permitted value | Serve it and elect one refresher |
| Cache miss | Load with single-flight protection |
| Refresh fails | Serve stale only inside the maximum stale window |
| Maximum stale age exceeded | Fail or load synchronously according to product policy |
Trade-offs
Stale-while-revalidate protects origin capacity and latency by weakening freshness. A distributed lock adds failure modes; no lock risks a stampede. Sensitive or rapidly changing data may not tolerate stale service.
Decision rule: Serve stale only when the business cost of bounded staleness is lower than the availability cost of an origin surge.
Further reference
Browse all engineering snippets · Read about cache consistency