1. The Cache Stampede (Dogpiling) Problem
When a hot cache key (e.g., homepage configuration or celebrity profile at 50,000 QPS) expires at $t = 60.000\text{s}$, thousands of concurrent worker threads see a cache miss simultaneously. If every thread independently queries the primary database to recompute the value, database CPU spikes to 100% and p99 latency collapses—a failure mode known as a Cache Stampede or Thundering Herd.
Three Defense Layers Combined in This Build
- In-Flight Request Coalescing (
SingleflightGroup): Inspired by Go'sgolang.org/x/sync/singleflight. AMap<key, { promise, callers }>ensures that while a key is being fetched, all subsequent requests for the same key await the exact same Promise. - Dual-Horizon Stale-While-Revalidate (
softTtlMs+hardTtlMs): Entries have both a soft expiration and a hard expiration. BetweensoftExpireAtandhardExpireAt, callers immediately receive the slightly stale value ($0\text{ms}$ blocking latency) while a single coalesced background task refreshes the cache. - Probabilistic Early Expiration (XFetch): Using Vattani et al.'s optimal stochastic formula: $$\text{Recompute when } t - \Delta \cdot \beta \cdot \ln(\text{rand}()) \ge \text{softExpireAt}$$ Keys that take longer to recompute ($\Delta$) probabilistically trigger a background refresh before
softExpireAt.
2. Exception & Cancellation Hygiene
A critical bug in naive Promise-memoization caches is forgetting to evict the key from the in-flight map when the Promise rejects—permanently poisoning the key with a rejected Promise. Using a try / finally block guarantees this._inFlight.delete(key) runs on both resolution and rejection.
3. Verification & Running the Lab
node demo.js
node test.js