Skip to content

Stale-While-Revalidate — Fastest Fresh Content Strategy

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Stale. We cover key concepts, practical examples, and best practices to help you master this topic.

Stale-while-revalidate serves cached content instantly while fetching fresh data in the background, combining sub-millisecond cache speed with up-to-date content for the best of both worlds.

What You'll Learn

By the end of this tutorial, you will understand how stale-while-revalidate works, when to use it, how to implement it with the Cache API, and how to handle revalidation timing and conflicts.

Why It Matters

Users want both speed and freshness. Cache-first is fast but serves stale data. Network-first is fresh but slow. Stale-while-revalidate eliminates this tradeoff: the user sees content instantly (from cache) while the app fetches updates silently in the background.

Real-World Use

A news PWA uses stale-while-revalidate for article pages. When a user opens an article, it renders instantly from cache. The service worker fetches the latest version from the network and updates the cache silently. If the user refreshes, they see the updated version. Reading feels instant even on slow connections.

How Stale-While-Revalidate Works

Stale-While-Revalidate Flow
    Request comes in
         ↓
    ┌──────────────────────────────────────────┐
    │  Check cache AND start network fetch     │
    │  simultaneously                           │
    └──────────────────────────────────────────┘
         ↓                           ↓
    ┌────────────┐            ┌──────────────┐
    │ Cache hit  │            │ Cache miss   │
    └─────┬──────┘            └──────┬───────┘
          ↓                          ↓
    Return cached               Wait for network
    immediately                       ↓
          ↓                     Return fresh
    Background network          (and cache it)
    completes after                  ↓
    rendering                   Done
          ↓
    Update cache with
    fresh response
          ↓
    Done (next request
    gets fresh data)

Think of stale-while-revalidate like a restaurant with a display case (cache) and a kitchen (network). When you order, they serve food from the display case immediately (instant). The kitchen prepares fresh food in the background and restocks the display case. Next time, you get fresh food that was prepared between orders.

Basic Implementation

self.addEventListener('fetch', event => {
    if (event.request.method !== 'GET') return;

    event.respondWith(
        caches.match(event.request).then(cachedResponse => {
            // Start network fetch in background
            const fetchPromise = fetch(event.request)
                .then(networkResponse => {
                    if (networkResponse.ok) {
                        const clone = networkResponse.clone();
                        caches.open('dynamic-cache').then(cache => {
                            cache.put(event.request, clone);
                            console.log('Background cache update:', event.request.url);
                        });
                    }
                    return networkResponse;
                })
                .catch(error => {
                    console.log('Background fetch failed:', error.message);
                    return cachedResponse;
                });

            // Return cached immediately, or wait for network if no cache
            if (cachedResponse) {
                console.log('Serving stale:', event.request.url);
                return cachedResponse;
            }

            console.log('No cache, waiting for network:', event.request.url);
            return fetchPromise;
        })
    );
});

Output:

Serving stale: https://example.com/api/articles
Background cache update: https://example.com/api/articles
Serving stale: https://example.com/styles/main.css
Serving stale: https://example.com/images/hero.jpg
No cache, waiting for network: https://example.com/api/new-feature
Background cache update: https://example.com/api/new-feature

Stale-While-Revalidate with Max-Age

You can control staleness by serving from cache only if it is recent enough:

self.addEventListener('fetch', event => {
    if (event.request.method !== 'GET') return;

    const MAX_AGE = 5 * 60 * 1000; // 5 minutes in milliseconds

    event.respondWith(
        caches.match(event.request).then(cachedResponse => {
            // Check if cached response is fresh enough
            const isStale = cachedResponse && !isFresh(cachedResponse, MAX_AGE);

            if (cachedResponse && !isStale) {
                console.log('Fresh enough, no revalidation needed');
                return cachedResponse;
            }

            // Fetch from network (for stale or missing cache)
            const fetchPromise = fetch(event.request)
                .then(networkResponse => {
                    if (networkResponse.ok) {
                        // Add timestamp header for freshness tracking
                        const headers = new Headers(networkResponse.headers);
                        headers.append('X-Cache-Timestamp', Date.now().toString());

                        const freshResponse = new Response(networkResponse.clone().body, {
                            status: networkResponse.status,
                            statusText: networkResponse.statusText,
                            headers: headers
                        });

                        caches.open('dynamic-cache').then(cache => {
                            cache.put(event.request, freshResponse);
                        });
                    }
                    return networkResponse;
                })
                .catch(() => cachedResponse);

            // Return stale cached immediately if available
            if (cachedResponse) {
                console.log('Serving stale (exceeded max-age):', event.request.url);
                return cachedResponse;
            }

            return fetchPromise;
        })
    );
});

function isFresh(response, maxAge) {
    const cachedTime = response.headers.get('X-Cache-Timestamp');
    if (!cachedTime) return false;
    return (Date.now() - parseInt(cachedTime)) < maxAge;
}

Stale-While-Revalidate with Progress Notification

You can notify the page when fresh data arrives:

self.addEventListener('fetch', event => {
    if (event.request.method !== 'GET') return;

    event.respondWith(
        caches.match(event.request).then(cachedResponse => {
            const fetchPromise = fetch(event.request)
                .then(networkResponse => {
                    if (networkResponse.ok) {
                        const clone = networkResponse.clone();
                        caches.open('dynamic-cache').then(cache => {
                            cache.put(event.request, clone);
                        });

                        // Notify page about fresh data
                        if (cachedResponse) {
                            self.clients.matchAll().then(clients => {
                                clients.forEach(client => {
                                    client.postMessage({
                                        type: 'CACHE_UPDATED',
                                        url: event.request.url,
                                        timestamp: Date.now()
                                    });
                                });
                            });
                        }
                    }
                    return networkResponse;
                })
                .catch(() => cachedResponse);

            return cachedResponse || fetchPromise;
        })
    );
});

Stale-While-Revalidate for Lists with Pagination

For paginated content, stale-while-revalidate works well because users typically start with page 1 and navigate forward:

const CACHE_NAME = 'list-cache-v1';

self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    // Apply to paginated list endpoints
    if (url.pathname.startsWith('/api/list')) {
        event.respondWith(
            caches.open(CACHE_NAME).then(cache => {
                return cache.match(event.request).then(cachedResponse => {
                    const fetchPromise = fetch(event.request)
                        .then(networkResponse => {
                            if (networkResponse.ok) {
                                cache.put(event.request, networkResponse.clone());
                            }
                            return networkResponse;
                        })
                        .catch(() => cachedResponse);

                    return cachedResponse || fetchPromise;
                });
            })
        );
    }
});

When to Use Stale-While-Revalidate

This Strategy is ideal for:

  • Content that changes but not critically: News articles, blog posts, product descriptions
  • API lists: Articles list, product catalog, search results
  • User profiles: Display name, avatar, bio (low sensitivity to staleness)
  • Configuration files: Feature flags, settings, theme preferences
  • Images that update occasionally: Avatar photos, cover images

Avoid stale-while-revalidate for:

  • Authentication tokens: Must be current
  • Financial data: Stock prices, account balances
  • Real-time collaboration: Chat messages, shared cursors
  • Form validation responses: Must reflect server state
  • Time-sensitive offers: Discount counts, auction timers

Cache Update Strategies

// Update notification in your page
navigator.serviceWorker.addEventListener('message', event => {
    if (event.data.type === 'CACHE_UPDATED') {
        console.log('Cache updated for:', event.data.url);

        // Show subtle indicator that content refreshed
        const indicator = document.getElementById('fresh-data-indicator');
        if (indicator) {
            indicator.textContent = 'Content updated';
            indicator.classList.add('visible');
            setTimeout(() => indicator.classList.remove('visible'), 3000);
        }
    }
});

Common Mistakes

  1. Serving stale data indefinitely. Without max-age checking, stale-while-revalidate serves data that is days or weeks old. Add a freshness threshold.
  2. Network fetch failing silently when cache exists. If the background fetch always fails, the cache is never updated. Monitor fetch failures and alert when they persist.
  3. Updating cache after page navigation. If the user navigates away before the background fetch completes, the cache is still updated. This is usually fine, but consider canceling unnecessary fetches.
  4. Not handling cache miss during background fetch. If there is no cached response and the network fetch fails, the user gets nothing. Handle this with a fallback.
  5. Conflicting updates for the same resource. If two requests for the same resource fire simultaneously, you may have race conditions. Debounce or deduplicate requests.

Practice Questions

  1. How does stale-while-revalidate differ from cache-first?
  2. What happens if the background revalidation fetch fails?
  3. Why would you add a max-age check to stale-while-revalidate?
  4. How can you notify the page that fresh data has been cached?
  5. What types of content should NOT use stale-while-revalidate?

Challenge: Implement stale-while-revalidate with a 2-minute max-age. Notify the page when fresh data arrives. Add deduplication so that if 5 requests for the same URL fire in quick succession, only one background fetch occurs.

FAQ

Does stale-while-revalidate work offline?

It works partially. The cached response is served immediately. The background fetch fails (offline), so the cache is not updated. The stale data remains until a successful fetch occurs.

How do I prevent showing stale data for too long?

Set a max-age threshold. If the cached response exceeds the max-age, show a loading indicator while waiting for the network response instead of serving stale data.

Can stale-while-revalidate update the UI automatically?

Yes. Use postMessage from the service worker to notify the page. The page JavaScript listens for CACHE_UPDATED messages and replaces content in-place.

Does this strategy waste bandwidth on background fetches?

Background fetches use bandwidth, but the response is small (usually JSON). The benefit of instant renders outweighs the bandwidth cost for most applications.

Can I use stale-while-revalidate with IndexedDB?

Yes. The same pattern works with IndexedDB. Serve data from IndexedDB immediately, fetch fresh data from the network, and update IndexedDB in the background.

Mini Project

Build a service worker that uses stale-while-revalidate for a news article API. Add a 5-minute freshness threshold. Notify the page when new articles are available in the cache. In the page JavaScript, listen for update messages and show a "New articles available" banner. Test by loading articles, updating the server data, and refreshing to see the banner.

What's Next

You understand the hybrid strategy. Now learn extreme cases with cache-only and network-only strategies for situations where you need strict control.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro