Invalidation, purging and what not to cache
How to retire a cached copy quickly, why fingerprinted URLs beat purging, and the responses that must never be stored.
Getting rid of a stale copy
There are three ways to invalidate: purge by URL, purge by tag or surrogate key, and purge everything. Purging one URL is precise; tag-based purging is what you want when a template change affects thousands of pages; purge-all is the emergency lever that also empties your cache and hammers the origin.
# purge one URL (provider-specific API shape)
curl -X POST "https://api.cdn.example/purge" \
-H "Authorization: Bearer $CDN_TOKEN" \
-d '{"urls":["https://example.com/assets/app.js"]}'
# tag the response at the origin, then purge by tag after a content update
# Surrogate-Key: article-1042 list-home
curl -X POST "https://api.cdn.example/purge" \
-H "Authorization: Bearer $CDN_TOKEN" \
-d '{"tags":["article-1042"]}'- Purge is not instant everywhere: propagation takes seconds to a minute, and some edges may still hold a copy briefly.
- Purging is a poor substitute for correct cache keys and a short TTL on HTML.
- Fingerprinted assets need no purging at all - a new filename is a new cache entry, and the old one expires harmlessly.
- After a purge, expect a burst of requests to the origin. Stagger large purges if the origin is not sized for it.
What must not be cached
| Response | Why it must not be shared | Correct header |
|---|---|---|
| Logged-in pages | Contains another user's data if served to the wrong person | private, no-store |
| Cart and checkout | Session-specific state that changes on every action | private, no-store |
Anything with Set-Cookie | The cookie would be handed to other visitors | private and strip cookies at the edge |
| Authentication endpoints | Cached tokens or redirects replay a previous login | no-store |
| Draft or preview URLs | Unpublished content becomes public and indexable | no-store plus access control |
| Personalised API responses | Prices and entitlements differ per account | private, max-age=0 |
| Error responses | A cached 500 keeps serving the failure after the fix | no-store for 5xx |
# origin: be explicit rather than relying on defaults
location /account/ {
add_header Cache-Control "private, no-store" always;
add_header X-Robots-Tag "noindex" always;
}
# edge: strip cookies from cacheable assets so they are not keyed on them
location /assets/ {
proxy_hide_header Set-Cookie;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}⚠️
Cache configuration is a security boundary, not only a performance setting. A shared cache holding one visitor's authenticated response is a data leak, and the leaked URL can be indexed by search engines before anyone notices.
A strategy that avoids purging
- Give every asset a content hash in its filename, and cache it for a year as
immutable. - Serve HTML with a short
s-maxageso a deploy is live within minutes without any purge. - Tag long-lived, frequently updated content (a product, an article) so a single change can be purged by key.
- Keep a documented purge procedure with a token that is stored in a secret manager, not in a script on someone's laptop.
- Watch the hit ratio after each change; a sudden drop usually means a cache key was altered by accident.
Sites that follow this pattern barely use purge at all - stale content is short-lived by construction, and changed content arrives on a fresh URL.
FAQ
How long does a purge take?
Usually seconds, but never assume it is atomic worldwide. Verify with
curl -sI and an Age header of 0 from more than one location before declaring it done.Why do users still see the old page after a deploy?
The browser cached the HTML. Lower
max-age on HTML (or use no-cache), keep assets fingerprinted, and issue a purge for the entry points.Related
Cache headers and cache keys Build and deploy workflows
Last refreshed 2026-09-18.