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.
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>;
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.
maxAge, return the old value immediately and start a background fetch you do not await.Date.now() - ts against maxAge, where ts is when the value was stored.get.invalidate(key) drops one entry and get.clear() drops all, forcing the next read to fetch.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.