skip to content

Users report that a CDN is still serving an old version of a page hours after it was updated. How do you confirm which cache tier is serving the stale copy, and how do purging and revalidation differ as fixes?

level: seniorimportance: should knowfreq 42%

answer

  1. Age = seconds held, cumulative across tiers
  2. vendor cache-status: HIT/MISS/STALE + POP
  3. bypass CDN, fetch origin, compare
  4. purge = push now; revalidate = pull at expiry
  5. no purge reaches the browser

basics

~20 s

Read the Age header and the CDN's cache-status header: a large Age means the edge is serving a stored copy, and Age accumulates across tiers. Purge actively evicts the edge copy now; revalidation only makes the edge ask the origin once its entry goes stale. Browser copies must expire on their own.

solid answer

~60 s

First localise the tier. Request the URL through the CDN and read **`Age`** - seconds since the origin generated the response - plus the vendor cache-status header. A large `Age` with a hit status means the edge is answering. Then request the origin directly: if it returns the new content, the origin is fine and the edge entry is the problem; if it returns the old content, the origin or its own cache is. Remember `Age` accumulates: each cache in the chain adds the time it held the response, so a two-tier CDN can report an `Age` larger than any single tier's residency, and freshness is judged as lifetime minus `Age`. The two remedies differ in who initiates. **Purge** is an out-of-band control-plane call that evicts or invalidates the object at the edge immediately - the next request goes to the origin. **Revalidation** is in-band: when an entry goes stale the edge sends a conditional request and gets 304 or new content. Purge is a push, revalidation is a pull, and only purge helps before the TTL expires. Neither reaches browser copies, which is why browser TTLs should stay short.

code

bash · 4 lines
bash
curl -sSI https://www.example.com/pricing | grep -i -E 'age|cache-control|etag|x-cache|cf-cache-status'

curl -sSI --resolve www.example.com:443:203.0.113.10 https://www.example.com/pricing \
  | grep -i -E 'age|cache-control|etag'

go deeper

for a junior

Know that Age shows how long a cached copy has been held and that purging removes the CDN's copy.

for a middle

Add the origin-versus-edge comparison, conditional revalidation with ETag and 304, and that Age accumulates.

for a senior

Drive the diagnosis end to end, pick hard versus soft purge with stampede risk in mind, and explain why browser copies are unreachable.

for a principal

Set the platform policy - long edge TTL with tag-based purge wired into publish, content-hashed immutable assets, and an explicit worst-case staleness budget per content class.

## Step one: find the tier "The CDN is stale" is a hypothesis. Confirm it before acting. **`Age`** is the header for this. It reports, in seconds, how long the response has been held in caches since the origin generated it. `Age: 0` (or absent) with a fresh body means the origin answered. `Age: 21600` means the edge has been replaying a six-hour-old copy. Crucially, `Age` is **cumulative across tiers**. A response that sat two hours in a regional shield and four hours in an edge POP arrives with `Age: 21600`. A cache computes remaining freshness as its lifetime (`s-maxage`, else `max-age`, else heuristic) minus the current age, so a response arriving at a downstream cache with a large `Age` may already be stale on arrival. This is exactly how a multi-tier CDN can surprise you: a long shield residency consumes the TTL before the edge ever gets a turn. Alongside it, essentially every CDN emits a vendor cache-status header naming HIT, MISS, EXPIRED, STALE and often the POP. Read it. Then bypass the CDN - resolve the origin address directly, or use the origin hostname - and fetch the same path. Now you know: - Origin new, edge old -> the edge entry is the problem; purge it. - Origin old too -> your deploy or the origin's own cache is the problem; purging the CDN will just re-cache the old bytes. - Both new but the user still sees old -> it is the browser copy, and nothing server-side will fix it before the browser TTL elapses. Also re-check the headers you actually shipped. A missing `Cache-Control` invites a CDN's heuristic freshness (commonly derived from `Last-Modified`), which can be far longer than you assumed. A stray `s-maxage` from a shared config, or a `Vary` that makes hits improbable, both show up here. ## Purge versus revalidate **Revalidation** is the protocol's own mechanism. When the stored entry becomes stale, the cache does not discard it: it asks the origin whether the copy is still good, sending `If-None-Match` with the stored `ETag` (or `If-Modified-Since`). A `304 Not Modified` refreshes the entry cheaply - no body transferred; anything else replaces it. Revalidation is *reactive*: it happens on the next request after staleness, on the cache's timetable, not yours. Directives like `proxy-revalidate` or `no-cache` force it to happen every time, at the cost of an origin round trip per request. **Purge** is a control-plane action outside the HTTP request path: you call the CDN's API to evict or invalidate an object, a URL prefix, or a tag/surrogate key. It is *proactive* - it takes effect on publish rather than on expiry. Two flavours matter: a hard purge deletes the object so the next request is a full miss; a soft purge (or "invalidate") marks it stale so the edge can still serve it while revalidating, protecting the origin from a stampede. A third option sits between them: **stale-while-revalidate**, which lets the cache serve the stale copy immediately and refresh in the background. It smooths the latency cliff at expiry but does not solve "I need this gone now". ## Choosing a strategy The strongest pattern for content you control is **long edge TTL plus purge on publish**. Set a large `s-maxage` or `CDN-Cache-Control` so the origin sees almost no traffic, and make the publish path emit a purge. Tag-based purging is what makes this practical at scale: attach a surrogate key per entity (`product-1234`) and purge by tag when the entity changes, rather than enumerating every URL that embedded it. Where purge is not available or the content is externally controlled, keep the edge TTL short enough that the worst-case staleness is acceptable and let revalidation do the work - conditional requests are cheap when the ETag is stable. And for assets, sidestep the problem entirely: content-hashed filenames make each version a distinct URL, so `max-age=31536000, immutable` is safe and no invalidation is ever needed. ## The part you cannot fix No purge reaches a browser cache. If you shipped `max-age=86400` on an HTML document and it is wrong, affected users keep it for up to a day regardless of what you do at the edge. This is the operational argument for short private TTLs paired with long shared TTLs - and, for HTML, often `no-cache` in the browser tier so every use is at least revalidated.

  • What exactly does the Age response header report, and why can it exceed the time the edge POP has held the object?
    Age is the number of seconds since the origin generated the response, as estimated by the caches that have held it. It is cumulative: each tier adds its own residency time before forwarding, so a shield cache's hours are included in the value the edge reports. That is also why a response can arrive at a downstream cache already stale, since remaining freshness is the lifetime minus the current age.
  • When would you choose a soft purge or stale-while-revalidate over a hard purge?
    When the object is hot and a full miss would send a burst of concurrent requests to the origin. A soft purge marks the entry stale so the edge keeps serving it while it revalidates once, and stale-while-revalidate does the same automatically at expiry. You trade a short window of slightly stale content for protection against a thundering herd.
  • The origin returns the new content and the edge has been purged, yet a user still sees the old page. What is left?
    Their browser cache. Private copies are unreachable from the server side and persist until max-age elapses or the user forces a reload. The structural fix is a short private TTL - often no-cache for HTML so every use is revalidated - with the long lifetime placed on the shared tier where purge works.

saying these in an interview costs you the question

  • Purging the CDN without checking the origin, so the stale bytes are simply re-cached
  • Believing a purge also clears browser caches
  • Reading Age as time in the current cache rather than cumulative time since origin generation
  • Assuming a response with no Cache-Control will not be cached, when heuristic freshness applies
  • Hard-purging a hot object with no stale-serving protection and stampeding the origin

context