TL;DR / Stale-while-revalidate serves cached content immediately to the user while fetching a fresh copy in the background, trading perfect freshness for instant perceived performance.
How It Works
2. Revalidate
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Client │─────────→│ Cache │─────────→│ Origin │
└────────────┘ └────────────┘ └────────────┘
↑ ↑ │
│ 1. Serve stale │ 3. Fresh response │
└───────────────────────└───────────────────────┘
Cache updated for
next request
The stale-while-revalidate pattern appears at two distinct levels in web development: as an HTTP cache directive defined in RFC 5861, and as a client-side data fetching strategy popularized by libraries like SWR and React Query. Both implement the same core idea but at different layers of the stack.
The HTTP Directive
Cache-Control: max-age=60, stale-while-revalidate=3600 tells the cache (browser, CDN, or intermediate proxy) two things. For the first 60 seconds, the response is fresh and can be served without any network request. After 60 seconds but within 3660 seconds total (60 + 3600), the response is stale but the cache may serve it immediately while making a background request to the origin to refresh the cached copy.
The key insight is the word "while." The stale response goes to the client now. The revalidation request goes to the origin simultaneously. The user sees content instantly. The cache updates asynchronously. The next request gets the fresh content.
After the stale-while-revalidate window expires (past 3660 seconds in this example), the cache must not serve the stale response. It blocks on the revalidation request, behaving like a normal cache miss.
Why It Matters for Performance
Without stale-while-revalidate, once max-age expires, the next request blocks on a full round trip to the origin. For a CDN edge node 50ms from the user but 200ms from the origin, the user suddenly experiences 200ms latency instead of a cache hit. For an API endpoint behind a slow database query, the user waits for the query to complete.
With stale-while-revalidate, the user always gets the cache-hit latency (50ms from the edge, or essentially zero from the browser cache). The staleness cost is that the data might be up to one request cycle out of date -- the user sees the previous version, and the next user (or the next request) sees the refreshed version.
This is acceptable for the vast majority of web content. Product listings, blog posts, user profiles, dashboard data, navigation menus -- all of these can tolerate being seconds to minutes stale in exchange for instant responses.
Client-Side SWR Libraries
Libraries like Vercel's SWR (useSWR hook) and TanStack Query implement the same pattern in JavaScript. On mount, they return the cached data immediately (if available) and fire a background fetch. The component renders instantly with cached data, then re-renders when fresh data arrives.
The client-side implementation adds features that HTTP stale-while-revalidate cannot provide. Automatic refetch on window focus detects when a user returns to a tab and refreshes stale data. Polling intervals provide periodic background updates. Mutation-triggered revalidation lets you invalidate and refetch specific cache keys after a write operation. Optimistic updates let you show the expected result immediately and reconcile when the server responds.
These libraries also manage deduplication: if three components request the same key simultaneously, one network request is made and all three receive the result. The in-memory cache is shared across the component tree.
Cache Key Design
The effectiveness of SWR depends entirely on cache key design. A key that is too broad (e.g., just "/api/users") means any user list request returns the same cached data regardless of filters or pagination. A key that is too narrow (e.g., including a timestamp) defeats caching entirely because every request generates a unique key.
The standard pattern is to include all parameters that affect the response: /api/users?page=2&sort=name&filter=active. This ensures that each unique query has its own cache entry, but repeated identical queries benefit from the stale-while-revalidate behavior.
The Thundering Herd Problem
At the CDN layer, stale-while-revalidate can exacerbate thundering herds. When a popular resource's max-age expires, many edge nodes simultaneously send revalidation requests to the origin. CDNs mitigate this with request coalescing (collapsing multiple identical origin requests into one) and stale-if-error fallback. Client-side libraries face a similar issue: if many browser tabs refocus simultaneously, all fire revalidation requests. Well-designed libraries add jitter (randomized delay) to spread the load.
Interaction with Service Workers
Service workers can implement stale-while-revalidate independently of HTTP headers. The worker intercepts a fetch, immediately responds from the cache, then makes a network request and updates the cache. Workbox's StaleWhileRevalidate strategy is the most common implementation, handling cache matching, network fetching, cache updating, and error fallback in a single configuration.
Gotchas
- The user always sees the previous version on the first request after cache expiry -- if data changed between the cache write and the revalidation, the user's current view is stale. Only the next request sees fresh data.
stale-while-revalidatewithoutmax-ageis meaningless -- the directive only governs behavior aftermax-ageexpires. Withoutmax-age, there is no freshness window, and the directive has no effect.- Client-side SWR libraries still need error handling for the background refetch -- if the revalidation fails, the stale data remains. Libraries like SWR expose
errorstate alongsidedata, but many developers only checkdata. - Background revalidation is not free -- every stale response triggers a network request. For high-traffic pages, this means the origin receives nearly the same request volume as if there were no cache, just delayed by the
max-agewindow. - Cache key serialization must be deterministic --
{ b: 2, a: 1 }and{ a: 1, b: 2 }are the same query but different cache keys if serialized withJSON.stringify. Use sorted key serialization or a canonical form.