Stale-While-Revalidate at the CDN Edge Layer
Stale-while-revalidate at the edge serves a cached payload instantly while an asynchronous background fetch refreshes it, decoupling user-facing latency from origin response time. For a headless CMS this cuts Time to First Byte and shields the origin from traffic spikes — but only with precise header composition, explicit routing boundaries, and disciplined invalidation. A misconfigured directive or an overlapping route breaks the contract, serving stale drafts or stampeding the origin.
The two directives that govern SWR
The behavior lives entirely in the Cache-Control response header, via two directives:
s-maxage— TTL for shared caches (CDN POPs, reverse proxies). After it expires, the edge marks the asset stale.stale-while-revalidate— grace period during which the edge keeps serving the stale payload while fetching a fresh copy.
Per RFC 5861, these are independent. A production header like public, s-maxage=300, stale-while-revalidate=86400 serves fresh content for five minutes, then stale content for up to 24 hours while refreshing in the background.
The cached entry moves through these states as the two windows elapse:
stateDiagram-v2 [*] --> Fresh: origin fetch cached Fresh --> Stale: s-maxage expires Stale --> Revalidating: request within SWR window Revalidating --> Fresh: background fetch succeeds Stale --> Expired: SWR window elapses Expired --> Revalidating: next request blocks on origin Revalidating --> Expired: background fetch fails
Routing matters as much as the header. Draft, preview, and authenticated endpoints must bypass the shared cache; see Content Delivery Network Routing Logic. Without path- or header-based isolation, an edge worker can cache a preview payload and serve a stale draft to an editor.
Edge worker implementation
Many CMS platforms omit SWR directives or apply blanket caching that conflicts with Jamstack routing. Cloudflare Workers, Vercel Edge Functions, and Fastly Compute@Edge need explicit header injection: SWR for public routes, full bypass for preview.
import type { ExecutionContext } from '@cloudflare/workers-types';
interface Env {
// Environment bindings if needed
}
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const url = new URL(request.url);
// Identify preview/draft traffic via query params or custom headers
const isPreview =
url.searchParams.has('preview') ||
request.headers.get('x-cms-mode') === 'draft' ||
request.headers.get('cookie')?.includes('cms_preview=1');
// Editorial traffic bypasses the shared cache
if (isPreview) {
return fetch(request, {
cf: { cacheTtl: 0, cacheEverything: false }
});
}
const originResponse = await fetch(request);
// Clone headers — origin headers are immutable
const modifiedHeaders = new Headers(originResponse.headers);
// Apply SWR only to successful, cacheable JSON
if (originResponse.ok && originResponse.headers.get('content-type')?.includes('application/json')) {
modifiedHeaders.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=86400');
// Strip headers that would fragment the cache key
modifiedHeaders.delete('set-cookie');
modifiedHeaders.delete('vary');
modifiedHeaders.set('Vary', 'Accept-Encoding');
}
return new Response(originResponse.body, {
status: originResponse.status,
statusText: originResponse.statusText,
headers: modifiedHeaders
});
}
};
The worker intercepts the request, forces a cache bypass on preview flags, proxies public routes to the CMS, then rewrites Cache-Control to enforce SWR regardless of origin config and sanitizes Vary/Set-Cookie to stop cache-key multiplication. Fastly VCL or Compute@Edge needs equivalent logic in vcl_recv and vcl_deliver; set beresp.stale_while_revalidate explicitly when origin headers are unreliable.
Stampedes and cache-key fragmentation
The most common cause of degraded edge performance is the cache stampede: concurrent requests during the revalidation window each trigger a background fetch, negating SWR’s origin shielding. Enable request coalescing in your CDN control plane — Cloudflare, Fastly, and CloudFront all serialize identical origin requests into a single fetch and queue the rest.
Then audit Vary for fragmentation. Vary: Cookie or Vary: Authorization multiplies cache keys per unique token, defeating SWR. Restrict Vary to low-cardinality headers like Accept-Encoding or Accept-Language. For client-side hydration and polling that complements the edge, see Data Fetching & Caching Strategies.
Webhook invalidation
Webhook-driven purges race during high-frequency publishing. A purge can miss if the request fails, routes to a secondary POP, or lands after the SWR grace period started. Use idempotent retries with exponential backoff, and cross-reference CDN purge logs against CMS deploy timestamps. If the edge returns X-Cache: HIT indefinitely after a publish, check that your response transformer isn’t stripping stale-while-revalidate or injecting a conflicting no-cache.
For GraphQL backends, layer Apollo Client GraphQL Caching or React Query for CMS Data at the client — the edge handles network latency, the client handles UI revalidation.
Crawlers and SEO
Crawlers often request pages mid-revalidation. Serving stale content to Googlebot during the stale-while-revalidate window is fine as long as the content stays semantically valid and the window is reasonable (typically under 24 hours for marketing pages). To stay crawl-efficient:
- Match
s-maxageto content update frequency — high-churn news sites need shorter TTLs. - Return accurate
Last-ModifiedandETagso crawlers validate without a full download. - Watch
Agein server logs; if crawlers consistently seeAgeabove yours-maxage, the grace period is too long for SEO-sensitive routes.
Strict Cache-Control composition, preview isolation, request coalescing, and synchronized webhook purges are what turn a latency-bound CMS into a low-TTFB delivery layer that holds up across Jamstack and Next.js ISR deployments.