Performance
stale-while-revalidate: how to configure it and how to test it
What `stale-while-revalidate` really does, how it combines with `stale-if-error`, and how to check the CDN serves the old copy while it refreshes.
Key points
- `stale-while-revalidate` serves the old copy and refreshes in the background: the user does not wait.
- It is not an infinite cache: the declared window is the longest you accept the old copy.
- Do not use it on personalised responses, mutations or data that must be exact to the second.
`stale-while-revalidate` (SWR) is a `Cache-Control` directive: for N seconds after expiry, the old copy can be served while a background request refreshes it. The user sees something immediately, and the CDN does its work afterwards.
The syntax
Cache-Control: public, max-age=60, stale-while-revalidate=300
The meaning: for 60 seconds the response is fresh; from second 61 to 360 the old copy can be served while the CDN regenerates it. After that, the request waits for a new response.
SWR and stale-if-error are not the same thing
| Directive | When it applies | What it risks |
|---|---|---|
| stale-while-revalidate | After expiry, with the backend reachable | Old data for the length of the window |
| stale-if-error | When the backend returns an error | Showing old data during an outage |
| max-age | Before expiry | Nothing: it is the fresh window |
| no-store | Always | Losing every caching benefit |
Combine them only when you know what you are promising: `max-age=60, stale-while-revalidate=300, stale-if-error=600` means an error can extend the life of old data.
Where it makes sense
- Catalogue lists that change slowly.
- Images and assets with versioned URLs.
- Public responses that are identical for everyone.
- Marketing pages with no personal data.
Where it does not
- Responses with user data or tokens.
- Mutations and endpoints that change state.
- Sales or prices that must be exact to the minute.
- Responses that depend on cookies or geolocation.
How to test it
Do not trust the header: look at what the edge returns. Two requests in quick succession must show the same version; after the window, the second one may still show the old copy while the CDN regenerates.
curl -sI https://example.com/api/catalog | grep -i "cache-control\|age\|x-cache"
curl -sI https://example.com/api/catalog | grep -i "age\|x-cache"
Check `Age` and the provider header (`x-cache`, `cf-cache-status`). If `Age` does not grow, the CDN is not serving from cache and the directive is only decoration.
The real case of the catalogue
The public EneUI lists use a short cache with a stale window: the catalogue is public, identical for everyone and changes with the harvest cycles. With 111,985 components (31 October 2026) serving from cache is not a detail: it is what keeps the site up on a small machine.
Frequently asked questions
Does SWR refresh the cache in the background?
Yes, when a request arrives after expiry: it serves the old copy and starts the refresh. There is no separate job.
Can I use it on an API with login?
No, if the response is personalised and the CDN is shared. Serving one user's response to another is an incident, not a performance bug.
How long should the window be?
As long as the product tolerates old data. There is no universal number: declare the tolerance and configure accordingly.
Does SWR work for images too?
Yes, often through the CDN. Assets with versioned URLs are the safest case, because the content never changes under the same URL.
How do I verify it works on Vercel or Cloudflare?
Look at the provider cache status headers and the `Age` header. If they are missing, the directive was not applied to that path.