Cache hierarchy: browser, edge and origin caches
How the three cache layers interact, why a browser hit still helps, the revalidation flow, proxy caches, and designing so the edge absorbs the traffic.
Three layers, one request
| Layer | Lives in | Cheapest to hit | Controlled by |
|---|---|---|---|
| Browser cache | The user's device | Yes - no network at all | Cache-Control and the URL |
| Intermediate proxy | Corporate or ISP proxy | Usually | The same headers, unless it misbehaves |
| Edge cache | The CDN point of presence | Fast - one short round trip | Cache rules and s-maxage |
| Origin shield | A designated CDN location | Moderate | CDN configuration |
| Origin | Your server | Most expensive | Application caching and database tuning |
What a request looks like at each step
1. browser has a fresh copy -> no request leaves the device
2. browser copy is stale -> conditional request with If-None-Match
or If-Modified-Since
3. edge has a fresh copy -> 200 from the edge, Age: n
4. edge copy is stale -> revalidate with the shield or origin
5. shield has a fresh copy -> 200 from the shield
6. nothing has it -> origin renders, everyone stores it- A browser hit is the only hit that costs nothing. Design the headers so a repeat visit reads from disk.
- A 304 response still costs a round trip. It saves bandwidth, not latency - so give static assets a long lifetime instead of relying on revalidation.
- Each layer can only be as fresh as the headers allow. If the origin sets
no-cache, every layer above it revalidates on every request.
Revalidation and stale serving
# HTML that must be current but should not block the user
Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=86400, stale-if-error=3600
# a fingerprinted asset that will never change
Cache-Control: public, max-age=31536000, immutable
# an API response that is safe for a minute at the edge, never in the browser
Cache-Control: private, no-store, s-maxage=60
# an authenticated response that must never be shared
Cache-Control: private, no-store
Vary: Authorization, Cookiemax-age=0pluss-maxageis a deliberate split: the browser revalidates while the edge serves from cache.stale-while-revalidatelets the edge return a slightly stale copy immediately and refresh in the background - the single biggest perceived-latency win available.stale-if-errorserves a stale copy when the origin is failing, which turns an outage into a slightly old page.immutabletells the browser not to revalidate even on a manual reload, which is correct only for a URL that contains a content hash.
# see what a second request looks like
curl -sI https://example.com/ | grep -iE "cache-control|age|etag|vary"
curl -sI https://example.com/ | grep -iE "cache-control|age|etag|vary"
# force a revalidation and watch for a 304
curl -sI -H 'If-None-Match: "<etag-from-above>"' https://example.com/ | head -3Designing for edge absorption
- Decide which URLs are cacheable at all. HTML with a session-specific header is not, and no rule will make it so.
- Give static assets content-hashed names so their lifetime can be a year.
- Split the page: a cached shell plus a small personalised fragment fetched separately.
- Set the cache key to the smallest set of inputs that changes the response. Each extra input multiplies the number of cached objects.
- Measure the ratio of hits at the edge versus requests reaching the origin. A hit ratio below 80 percent on static assets usually means the key is too specific.
- Watch the shield separately. If the shield is hitting origin as often as the edge hits the shield, tiering is not happening.
| Symptom | Layer | Likely cause |
|---|---|---|
Age: 0 on every response | Edge | Not cacheable: no-store, cookies, or a private response |
| Hit ratio around zero on images | Edge | Cache key includes a query string or a tracking parameter |
| Version number in the URL | Browser | No content hashing, so lifetime must stay short |
| Different users get the same page | Edge | A personalised response cached without Vary |
| 304 on every navigation | Browser | Long max-age missing on the asset, so it revalidates |
| Origin load unchanged | Shield | No shield, or the shield is not being used by the edge |
💡
The cheapest CDN optimisation is not a bigger plan, it is a better cache key. Strip tracking parameters, ignore headers that do not change the response, and normalise the URL before it reaches the cache - then compare the request count at the origin before and after.
FAQ
Should the browser cache HTML?
Usually for a short time or not at all, with the edge caching it instead. Users expect a reload to show new content, and browsers treat reloads specially anyway.
Why is my hit ratio low on an app with query parameters?
Every distinct query string is a distinct cache key. Normalise or strip parameters that do not affect the response, or move them out of the URL.
Related
Cache headers and cache keys Measuring CDN performance and debugging cache issues
Last refreshed 2026-09-18.