SWR Stale-While-Revalidate Patterns

Stale-while-revalidate serves cached content immediately, then refreshes it in the background — bridging instant UI response with eventual consistency. Codified for HTTP caching in RFC 5861 and adapted to frontend state management, SWR solves a core headless CMS problem: show content now, fetch the fresh payload after. Within Data Fetching & Caching Strategies for decoupled platforms, it’s the cache-first baseline that minimizes layout shift and perceived latency.

The lifecycle

SWR runs a three-phase cycle suited to edge-rendered content:

  1. Serve stale: return cached data immediately on mount, hydration, or route transition.
  2. Revalidate: fire an async fetch to the CMS API or upstream cache.
  3. Update: replace the stale payload with fresh data via targeted reconciliation, no full reload.

The three phases run in this order on every read:

flowchart LR
  A["Mount / hydration / route change"] --> B["Serve stale from cache"]
  B --> C["Render immediately"]
  B --> D["Revalidate: async fetch to CMS"]
  D --> E{"Payload changed?"}
  E -->|yes| F["Update via targeted reconciliation"]
  E -->|no| G["Keep current render"]
  F --> H["Re-render, no full reload"]

This fits content-heavy apps where editorial updates are asynchronous and users rarely need sub-second freshness. Decoupling render from network latency lets editors publish without breaking the frontend performance budget.

Implementation

A production SWR integration needs standardized fetchers, deterministic keys, and centralized config. This blueprint is framework-agnostic (React, Vue, Svelte).

JavaScript
// lib/cms-fetcher.js
// Standardized fetcher with CMS auth, error normalization, and JSON parsing
export const cmsFetcher = async (url) => {
  const res = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${process.env.NEXT_PUBLIC_CMS_ACCESS_TOKEN}`,
      'Accept': 'application/json'
    },
    next: { revalidate: 60 } // Edge cache hint (Next.js specific)
  });

  if (!res.ok) {
    const err = new Error(`CMS Fetch Failed: ${res.status}`);
    err.status = res.status;
    err.info = await res.json().catch(() => null);
    throw err;
  }

  return res.json();
};

// lib/swr-config.js
// Centralized configuration for predictable cache behavior
export const swrConfig = {
  revalidateOnFocus: false,
  revalidateOnMount: true,
  dedupingInterval: 5000,
  errorRetryCount: 2,
  errorRetryInterval: 1000,
  keepPreviousData: true,
  suspense: false
};

Cache keys

Keys must be deterministic and content-addressable. Don’t put timestamps or random IDs in a key unless you explicitly want cache busting.

JavaScript
// ✅ Deterministic
const cacheKey = `/api/cms/pages/${slug}?locale=${locale}&depth=3`;
// ❌ Non-deterministic (breaks cache sharing)
const badKey = `/api/cms/pages/${slug}?t=${Date.now()}`;

Configuration tradeoffs

Flag Behavior Tradeoff Use case
revalidateOnFocus Refetches on window focus More API calls; UI flash Admin dashboards, preview, collaborative editing
refreshInterval Polls at a fixed interval Predictable overhead; stale gaps Live blogs, editorial preview, real-time feeds
dedupingInterval Drops duplicate requests in a window Slightly delayed fresh data on concurrent mounts Concurrent-request scenarios
keepPreviousData Retains stale data during revalidation No loading state, but shows old data briefly Pagination, infinite scroll, lists
focusThrottleInterval Throttles focus-triggered revalidations Less network noise on rapid tab switching High-traffic public sites, mobile

Full options in the SWR configuration docs.

Cache management and integration

Programmatic revalidation

When a CMS webhook fires, target the specific key with mutate instead of reloading:

JavaScript
import useSWR, { mutate } from 'swr';

// Triggered by CMS webhook payload
export async function invalidateCMSContent(slug) {
  const key = `/api/cms/pages/${slug}?locale=en`;
  await mutate(key, undefined, { revalidate: true });
}

For full webhook-to-cache workflows, see Implementing SWR cache revalidation for dynamic content.

When to choose SWR

SWR fits REST or simplified GraphQL endpoints where normalization happens upstream. For highly relational graphs needing query batching and field-level normalization, teams move to React Query for CMS Data or Apollo Client GraphQL Caching. SWR stays optimal when:

  • Payloads are flat or pre-flattened at the edge
  • Bundle-size limits rule out a heavy GraphQL client
  • Cache-first hydration matters more than complex query composition

Aligning with other layers

SWR runs at the client but must align upstream:

Deduplication tuning for multi-island layouts is covered in SWR deduplication for concurrent headless requests.

Deployment checklist