Stale-While-Revalidate
Stale-while-revalidate
Section titled “Stale-while-revalidate”Stale-while-revalidate (SWR) is the best of both worlds: the service worker returns a cached response instantly — so the user never waits for the network — and simultaneously kicks off a background network fetch to update the cache for next time.
On the very first request for a resource, there is nothing in the cache, so the SW waits for the network response, caches it, and returns it. From the second request onwards, the cached (possibly stale) copy is returned immediately while a fresh copy is silently fetched in the background. The user sees content right away and will be on the latest version on their very next visit.
This pattern is a natural fit for resources that update occasionally but where instant load time matters more than pixel-perfect freshness: user avatars, news feeds, config JSON, CSS files, and web fonts are all good candidates. It is a poor fit for real-time data — stock prices, live scores, or anything where being one version behind is unacceptable.
How SWR works
Section titled “How SWR works”- The service worker receives the
fetchevent. - It opens the cache and checks for an existing response.
- It kicks off a network fetch regardless of whether the cache has a response (background revalidation).
- If there is a cached response it is returned immediately to the page; if there is no cached response the handler waits for the network and returns that instead.
- When the network response arrives, the cache entry is silently updated so the next request gets the freshest copy.
const CACHE = 'swr-v1';
self.addEventListener('fetch', (event) => { event.respondWith( caches.open(CACHE).then((cache) => cache.match(event.request).then((cached) => { const networkFetch = fetch(event.request).then((response) => { cache.put(event.request, response.clone()); return response; }); // Serve cache instantly; fall back to network on miss return cached || networkFetch; }) ) );});sequenceDiagram participant P as Page participant SW as Service Worker participant C as Cache Storage participant N as Network P->>SW: fetch(request) SW->>C: cache.match(request) SW->>N: fetch(request) [background] C-->>SW: cached Response (may be stale) SW-->>P: Response (instant) N-->>SW: fresh Response SW->>C: cache.put(request, fresh.clone()) Note over SW,C: Cache updated for next request
Cache-then-network (page-side SWR)
Section titled “Cache-then-network (page-side SWR)”There is also a page-side variant of this pattern. The page fires both a cache lookup and a network fetch at the same time, renders the cached result immediately, and then updates the UI when the network response arrives — no service worker required.
async function fetchWithCacheThenNetwork(url) { const cache = await caches.open('swr-v1'); const cached = await cache.match(url);
// Start the network request immediately, don't await yet const networkPromise = fetch(url).then((response) => { cache.put(url, response.clone()); return response.json(); });
// Render the stale cached version right away if available if (cached) { const staleData = await cached.json(); renderUI(staleData); // show immediately }
// Update the UI when the fresh data lands const freshData = await networkPromise; renderUI(freshData);}This is useful in frameworks where the component can re-render when fresh data arrives, giving a progressively improving experience without any visible loading spinner.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| Stale-while-revalidate | Instant response from cache with a background update — speed now, freshness next round | The user always sees data that is one round behind; not suitable for data that must be real-time |
| Pure cache-first | Simpler, no background fetch to manage | No auto-refresh mechanism — data stays stale until the cache is invalidated another way |
Common mistakes
Section titled “Common mistakes”- Not awaiting the background cache update (a mistimed fire-and-forget), so the browser can terminate the service worker before the fetch finishes — wrap it in
event.waitUntil(). - Using SWR for data that must always be 100% correct (an account balance, say), so the user sees an old number without realizing it.
- Never trimming old cache entries, so storage grows forever because every revalidation overwrites but nothing ever cleans up unused entries.
The stale-while-revalidate HTTP Cache-Control directive (RFC 5861; supported by CDNs including Cloudflare and Fastly) is the standard pattern this lesson’s service-worker version borrows its name and idea from — the CDN layer did it first, before service workers brought the same idea to the client.