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

LayerLives inCheapest to hitControlled by
Browser cacheThe user's deviceYes - no network at allCache-Control and the URL
Intermediate proxyCorporate or ISP proxyUsuallyThe same headers, unless it misbehaves
Edge cacheThe CDN point of presenceFast - one short round tripCache rules and s-maxage
Origin shieldA designated CDN locationModerateCDN configuration
OriginYour serverMost expensiveApplication 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, Cookie
  • max-age=0 plus s-maxage is a deliberate split: the browser revalidates while the edge serves from cache.
  • stale-while-revalidate lets the edge return a slightly stale copy immediately and refresh in the background - the single biggest perceived-latency win available.
  • stale-if-error serves a stale copy when the origin is failing, which turns an outage into a slightly old page.
  • immutable tells 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 -3

Designing for edge absorption

  1. Decide which URLs are cacheable at all. HTML with a session-specific header is not, and no rule will make it so.
  2. Give static assets content-hashed names so their lifetime can be a year.
  3. Split the page: a cached shell plus a small personalised fragment fetched separately.
  4. Set the cache key to the smallest set of inputs that changes the response. Each extra input multiplies the number of cached objects.
  5. 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.
  6. Watch the shield separately. If the shield is hitting origin as often as the edge hits the shield, tiering is not happening.
SymptomLayerLikely cause
Age: 0 on every responseEdgeNot cacheable: no-store, cookies, or a private response
Hit ratio around zero on imagesEdgeCache key includes a query string or a tracking parameter
Version number in the URLBrowserNo content hashing, so lifetime must stay short
Different users get the same pageEdgeA personalised response cached without Vary
304 on every navigationBrowserLong max-age missing on the asset, so it revalidates
Origin load unchangedShieldNo 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.

Cache headers and cache keys Measuring CDN performance and debugging cache issues

Last refreshed 2026-09-18.