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:
- Serve stale: return cached data immediately on mount, hydration, or route transition.
- Revalidate: fire an async fetch to the CMS API or upstream cache.
- 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).
// 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.
// ✅ 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:
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:
- Next.js ISR Implementation: pair
revalidateheaders with SWR polling so static generation and client freshness windows agree. - Content Delivery Network Routing Logic: match the CDN’s
stale-while-revalidateto SWR’sdedupingIntervalto avoid hammering the origin during spikes. - Automated Testing for Headless Integrations: mock
fetchand assert stale-to-fresh transitions under network latency.
Deduplication tuning for multi-island layouts is covered in SWR deduplication for concurrent headless requests.