Skip to content

Stale-While-Revalidate — Serving Stale Content While Fetching Fresh Data

DodaTech Updated 2026-06-28 6 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 immediately to users while triggering a background refresh, balancing speed with eventual consistency.

What You'll Learn

By the end of this tutorial, you'll understand the stale-while-revalidate pattern, how it works in ISR vs CDN caching, how to configure it in Next.js and CDN headers, and when to use it.

Why It Matters

Users hate waiting. Stale-while-revalidate ensures they never do. Instead of blocking the request while fetching fresh data, you serve what you have (even if slightly stale) and update in the background. The next Visitor gets the fresh version.

Real-World Use

A news website uses stale-while-revalidate for its homepage. When a reader visits during the revalidation window, they see the cached page instantly. Behind the scenes, the server fetches the latest headlines. The next visitor sees the updated page.

SWR Architecture

graph LR
    A[User Request] --> B{Cache exists?}
    B -->|No| C[Fetch fresh data
blocking] B -->|Yes| D{Stale?} D -->|No| E[Serve fresh
cache] D -->|Yes| F[Serve stale
immediately] F --> G[Background
fetch new data] G --> H[Update cache
with fresh data] H --> I[Next visitor
gets fresh data] C --> J[Cache result] J --> E style F fill:#f39c12,color:#fff style G fill:#4a90d9,color:#fff style H fill:#27ae60,color:#fff

Next.js ISR with SWR

// pages/posts/[slug].js — SWR through ISR
export async function getStaticProps({ params }) {
    const res = await fetch(`https://api.example.com/posts/${params.slug}`);
    const post = await res.json();

    return {
        props: {
            post,
            // When this was actually generated
            generatedAt: Date.now(),
            // Max age before considering stale
            maxAge: 60
        },
        // After 60 seconds, serve stale + revalidate
        revalidate: 60
    };
}

export default function Post({ post, generatedAt, maxAge }) {
    const age = Math.floor((Date.now() - generatedAt) / 1000);
    const isStale = age > maxAge;

    return (
        <article>
            <h1>{post.title}</h1>

            {/* Show freshness indicator */}
            <div className={`freshness ${isStale ? 'stale' : 'fresh'}`}>
                {isStale
                    ? 'This page is being refreshed...'
                    : 'Content is up to date'
                }
            </div>

            <p className="meta">
                Generated: {new Date(generatedAt).toLocaleString()}
                (Age: {age}s)
            </p>

            <div>{post.content}</div>
        </article>
    );
}

CDN Cache-Control with SWR

// pages/api/with-swr.js — CDN SWR headers
export default async function handler(req, res) {
    const data = await fetchData();

    // Set SWR headers for CDN
    res.setHeader(
        'Cache-Control',
        'public, s-maxage=60, stale-while-revalidate=300'
    );

    res.json(data);
}

// Explanation of Cache-Control values:
// s-maxage=60: CDN considers cache fresh for 60 seconds
// stale-while-revalidate=300: CDN serves stale for up to 300 seconds
//   while revalidating in background

// Combined with Next.js ISR on pages:
export async function getStaticProps({ params }) {
    // ... fetch data ...

    return {
        props: { data },
        revalidate: 60 // Next.js ISR window
    };
}

// But the page also needs proper CDN headers in next.config.js
// next.config.js
module.exports = {
    async headers() {
        return [
            {
                source: '/:path*',
                headers: [
                    {
                        key: 'Cache-Control',
                        value: 'public, s-maxage=60, stale-while-revalidate=300'
                    }
                ]
            }
        ];
    }
};

Client-Side SWR Pattern

// components/StaleDataDisplay.jsx
import { useState, useEffect } from 'react';

export default function StaleDataDisplay({ initialData, fetchUrl, interval = 60000 }) {
    const [data, setData] = useState(initialData);
    const [lastUpdated, setLastUpdated] = useState(Date.now());
    const [isStale, setIsStale] = useState(false);

    useEffect(() => {
        const checkFreshness = setInterval(() => {
            const age = Date.now() - lastUpdated;
            setIsStale(age > interval);
        }, 1000);

        return () => clearInterval(checkFreshness);
    }, [lastUpdated, interval]);

    useEffect(() => {
        let mounted = true;

        async function refresh() {
            const start = Date.now();
            console.log(`[SWR] Background refresh starting...`);

            try {
                const res = await fetch(fetchUrl);
                const newData = await res.json();

                if (mounted) {
                    setData(newData);
                    setLastUpdated(Date.now());
                    setIsStale(false);
                    console.log(`[SWR] Refresh complete (${Date.now() - start}ms)`);
                }
            } catch (err) {
                console.error('[SWR] Refresh failed, keeping stale data:', err);
            }
        }

        // Refresh on interval
        const refreshTimer = setInterval(refresh, interval);

        return () => {
            mounted = false;
            clearInterval(refreshTimer);
        };
    }, [fetchUrl, interval]);

    const age = Math.floor((Date.now() - lastUpdated) / 1000);

    return (
        <div className={`swr-container ${isStale ? 'refreshing' : ''}`}>
            {isStale && (
                <div className="stale-banner">
                    Refreshing data... Showing last known values.
                </div>
            )}
            <pre>{JSON.stringify(data, null, 2)}</pre>
            <small>Data age: {age}s</small>
        </div>
    );
}

SWR with Different Strategies

// Different SWR strategies for different content needs
const swrStrategies = {
    // Strategy 1: Short stale, fast refresh
    news: {
        description: 'Breaking news needs quick updates',
        maxAge: 30,        // Fresh for 30 seconds
        staleLimit: 120,   // Serve stale for up to 2 minutes
        cacheControl: 'public, s-maxage=30, stale-while-revalidate=120'
    },

    // Strategy 2: Medium stale, reliable
    blog: {
        description: 'Blog posts are stable but accept updates',
        maxAge: 300,       // Fresh for 5 minutes
        staleLimit: 1800,  // Serve stale for up to 30 minutes
        cacheControl: 'public, s-maxage=300, stale-while-revalidate=1800'
    },

    // Strategy 3: Long stale, rarely changes
    docs: {
        description: 'Documentation updates are infrequent',
        maxAge: 3600,      // Fresh for 1 hour
        staleLimit: 86400, // Serve stale for up to 24 hours
        cacheControl: 'public, s-maxage=3600, stale-while-revalidate=86400'
    }
};

// Apply strategy based on content type
function getSWRConfig(contentType) {
    return swrStrategies[contentType] || swrStrategies.blog;
}

Common Mistakes

  1. Not setting stale-while-revalidate on the CDN. ISR alone doesn't set CDN caching headers. You must configure both ISR (Next.js) and Cache-Control (CDN) for full SWR behavior.
  2. Setting s-maxage equal to stale-while-revalidate. If both are 300, the CDN always considers the cache fresh for 300 seconds, never entering SWR mode. stale-while-revalidate should be longer than s-maxage.
  3. Using stale-while-revalidate with user-specific content. SWR is for public content. User-specific data should use SSR. Don't cache personalized pages on the CDN.
  4. Not handling the case where revalidation fails. When background revalidation fails, the stale data continues serving. Implement fallback mechanisms and monitoring.
  5. Forgetting to invalidate the CDN cache after revalidation. Next.js revalidates its internal cache, but the CDN may still serve old content. Use purge APIs or short TTLs.

Practice Questions

  1. What does stale-while-revalidate mean in the context of CDN caching?
  2. How does Next.js ISR implement the stale-while-revalidate pattern?
  3. What is the difference between s-maxage and stale-while-revalidate?
  4. When should you avoid using stale-while-revalidate?
  5. How do you monitor the state of stale-while-revalidate content?

Challenge: Create a benchmark that compares SSG, ISR with SWR, and SSR for the same content. Measure TTFB, content freshness, and server load for each approach over 1000 simulated requests.

FAQ

What happens if stale-while-revalidate exceeds its limit?

After the stale-while-revalidate window expires, the CDN treats the content as fully stale and either revalidates synchronously or returns an error. Set this value high enough.

Does stale-while-revalidate work with all CDNs?

Most major CDNs (Cloudflare, Akamai, Fastly) support stale-while-revalidate. Vercel's Edge Network also supports it. S3 + CloudFront requires custom configuration.

How do I know if my page was served stale or fresh?

Examine the Age response header. Age: 0 = fresh from origin. Age: 120 = served from cache for 120 seconds. Compare with s-maxage to determine stale status.

Can I show a visual indicator when content is stale?

Yes. Include the generation timestamp in the rendered HTML. Compare with current time client-side to display a 'stale' indicator.

Does SWR work with server-side rendered pages?

Yes. Add Cache-Control headers to SSR responses. The CDN will cache them and apply the same stale-while-revalidate pattern.

Mini Project

Build a dashboard that demonstrates stale-while-revalidate: create 3 pages with different SWR strategies (30s/120s, 300s/1800s, 3600s/86400s), show a real-time stale/fresh indicator, measure the TTFB for each Strategy under load, and verify CDN caching behavior.

What's Next

You've mastered the stale-while-revalidate pattern. Now learn about Revalidate Tag for tag-based revalidation in Next.js.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro