CDN Caching
Date: 2026-08-16
Copies of your responses held close to users, so requests don’t cross the world to your origin. The hard parts aren’t the caching — they’re the cache key and the invalidation, and both fail quietly.
What it is
A content delivery network (CDN) is a distributed set of edge servers that store and serve copies of your responses. A request is answered by the nearest edge holding a valid copy; only a miss reaches your origin.
Two benefits, and the second is usually the bigger one:
- Distance. Round trips are physics — an edge 20ms away beats an origin 200ms away — see Latency and Bandwidth
- Origin offload. A high hit rate means your servers handle a fraction of traffic, which is capacity and cost as well as speed
The cache key
The single most important concept, and the one that causes the most surprises.
The cache key determines what counts as “the same request”. By default it’s usually the URL, but everything included in it multiplies the number of stored copies.
key = URL only
/product/socks → 1 cached object, high hit rate
key = URL + all query strings
/product/socks
/product/socks?utm_source=email → separate objects for every campaign
/product/socks?fbclid=xyz123 → hit rate collapses
key = URL + cookie
→ one cached object per user → caching effectively disabled
Tracking parameters are the classic destroyer of hit rate. Every utm_*, gclid and fbclid variant creates a distinct cached object for content that is byte-identical. The fix is to strip marketing parameters from the cache key while still passing them to the page — most CDNs support an ignore-list, and it’s often the single highest-value CDN setting on a retail site.
Same for cookies: many CDNs skip caching entirely for requests carrying cookies. One analytics cookie set on / can disable caching site-wide.
s-maxage and shared caches
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
max-age=0— browsers always checks-maxage=300— the CDN may serve it for five minutes without touching the originstale-while-revalidate— after that, serve stale immediately and refresh behind
This combination is what makes HTML cacheable at the edge while staying fresh in the browser, and it’s the setting that most improves Time to First Byte — see HTTP Caching.
Invalidation
The genuinely hard part, and there are only three approaches:
| Approach | Works for | Cost |
|---|---|---|
| Short TTL | Content that tolerates being minutes stale | Lower hit rate, more origin load |
| Purge on publish | Content with a clear change event | Purge latency and API complexity |
| Fingerprinted URLs | Static assets | None — the URL changes, so nothing to purge |
Fingerprinting is the only one with no failure mode, which is why the pattern is universal for JS, CSS and images. Anything that can be fingerprinted should be.
For HTML, tag-based purging — where responses carry surrogate keys like product-4471 and a publish purges by tag — is the workable approach on commerce platforms with thousands of pages sharing components.
Personalisation vs caching
The tension that decides architecture on most retail sites.
Any per-user content in the HTML — a name, a basket count, personalised recommendations — makes the whole page uncacheable. The usual resolutions:
- Cache the shell, fetch the personal parts client-side. Simple, and it means the personal bits arrive late
- Edge-side personalisation — the CDN assembles cached fragments with fresh ones — Edge Computing
- Vary the cache key on a segment, not on the user. Ten segments is ten cached copies; ten million users is no caching
- Accept a lower hit rate on pages where personalisation genuinely earns more than speed costs
Watching it
- Hit rate, overall and by path. Below 80% for static assets means something is wrong with the key
x-cache/cf-cache-statusresponse headers tell you HIT, MISS, EXPIRED or BYPASS per request. BYPASS on a page you expected to cache usually means a cookie- Origin request volume. A rise with flat traffic means hit rate has dropped, often after a change to how parameters or cookies are handled
- Segment field data by country. A misconfigured region shows up as a slow tail nowhere else — Percentiles in Performance
Traps
- Caching a personalised page. The worst possible failure: one user’s basket or account details served to another.
privateandno-storeexist for this - Missing
Vary: Accept-Encoding, so a Brotli response reaches a client that can’t read it - Purging everything on every deploy, which empties the cache and hammers the origin at exactly the moment the new code is least proven
- Assuming a purge is instant. Propagation across a global network takes time, and it’s rarely zero