All questions

Stale-While-Revalidate Cache

Premium

Stale-While-Revalidate Cache

A stale-while-revalidate cache returns a cached value immediately — even after it has gone stale — while kicking off a background refresh so the next read gets fresh data. It is the caching strategy behind the HTTP stale-while-revalidate directive and data libraries like SWR and React Query: keep the UI fast by never blocking a read on the network, and let the data catch up a moment later.

Implement staleWhileRevalidateCache(fetcher, { maxAge }). It returns a get(key) function. A cached value counts as fresh for maxAge milliseconds and stale after that. A read resolves in one of three ways: a cold miss awaits fetcher(key) and caches it; a fresh hit returns the cached value with no fetch; a stale hit returns the old value right away and starts a background fetcher(key) whose result it does not await. If a background refresh for a key is already running, a second stale read must not start another.

Signature

type SwrGet<K, V> = {
  (key: K): Promise<V>;      // get(key) — resolves fresh or stale value
  invalidate(key: K): void;  // drop one cached entry
  clear(): void;             // drop every cached entry
};

function staleWhileRevalidateCache<K, V>(
  fetcher: (key: K) => Promise<V>,
  options: { maxAge: number }, // ms a cached value stays "fresh"
): SwrGet<K, V>;

Examples

let calls = 0;
const get = staleWhileRevalidateCache(
  async (key) => { calls++; return db.read(key); },
  { maxAge: 1000 },
);

await get('user:7'); // MISS  -> awaits fetcher, caches it.  calls === 1
await get('user:7'); // FRESH -> cached value, no fetch.      calls === 1
// ...1000ms pass, the entry is now stale...
await get('user:7'); // STALE -> resolves the OLD value now,
                     //          refreshes in the background. calls === 2
const get = staleWhileRevalidateCache(fetchProfile, { maxAge: 1000 });
await get('me'); // cache it
// ...maxAge passes, the entry is now stale...
const [a, b, c] = await Promise.all([get('me'), get('me'), get('me')]);
// a, b, and c are all the same stale value, returned immediately.
// Only ONE background fetchProfile('me') runs for the whole burst.

Notes

  • Instant reads — a hit, fresh or stale, resolves from the cache without waiting; only a cold miss awaits the fetcher.
  • Stale means serve-then-refresh — past maxAge, return the old value immediately and start a background fetch you do not await.
  • Dedupe refreshes — if a revalidation for a key is already in flight, do not start another; concurrent stale reads share one background fetch.
  • Freshness is wall-clock — compare Date.now() - ts against maxAge, where ts is when the value was stored.
  • Failures do not poison — a rejected background refresh keeps the stale value; a rejected cold miss rejects the caller and caches nothing, so the next read retries.
  • Escape hatchesget.invalidate(key) drops one entry and get.clear() drops all, forcing the next read to fetch.

FAQ

How is stale-while-revalidate different from a normal TTL cache?
A TTL cache makes the caller wait for a fresh fetch once the value expires. An SWR cache hands back the expired value instantly and refreshes it in the background, so reads stay fast and the data catches up on the next call.
What happens if the background revalidation fails?
The cached stale value is kept and keeps being served; the failed refresh is swallowed so it never surfaces to the caller, and the next read simply tries again.
Why dedupe background revalidations?
Without deduping, a burst of reads on a stale key would each start their own refresh, firing many identical requests at once. Tracking one in-flight fetch per key collapses the burst into a single round-trip.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium