Two layers, two different kinds of stale
When a change “isn’t showing”, there are two separate questions, and they have different answers and different fixes:
- Which server does the name lead to? That’s DNS. Stale DNS means a visitor’s resolver still has the old record cached and sends them to the old server.
- What does that server return? That’s HTTP caching. A stale cache means the visitor reaches the right place, a CDN edge or a reverse proxy, and it hands back a copy it stored before your change.
Then there’s a third layer people forget: the visitor’s own browser, with its HTTP cache, service workers and remembered redirects.
The fixes don’t transfer. Purging a CDN does nothing for a resolver that sends people to your old host. Waiting out a DNS TTL does nothing for an object cached with max-age=86400. So find the layer first.
One thing that changes the picture: if your site is behind a CDN proxy, DNS often doesn’t change at all. With a proxied Cloudflare record, the public answer is Cloudflare’s addresses whether your origin is in one data centre or another, and changing the origin takes effect at the edge directly. What people call “CDN propagation” is then one of two things: a configuration change still rolling out to the CDN’s edges, or cached objects that haven’t expired.
Step 1: is DNS settled?
Compare what your authoritative nameserver says with what public resolvers have cached:
$ dig @ns1.dns-provider.example www.example.com A +norecurse +short
198.51.100.20
$ dig @1.1.1.1 www.example.com A +short
198.51.100.20
$ dig @8.8.8.8 www.example.com A +short
203.0.113.5
Here Google’s resolver still has the old address. That’s DNS, and it’ll converge when that cached copy’s TTL runs out (dig @8.8.8.8 www.example.com A without +short shows the seconds remaining). The propagation check does the same comparison across six public resolvers at once, and the propagation guide covers how long it takes.
Behind a CDN, comparing IPs can mislead. CDNs answer with different addresses in different places on purpose, so resolvers disagreeing is normal. Compare the CNAME target instead (dig www.example.com CNAME +short), or check that every IP you get belongs to the CDN; the ASN lookup tells you who owns an address. If all answers point into the CDN, DNS is not your problem.
Step 2: take DNS out of the test
curl --resolve connects to an IP you choose while sending the right hostname and SNI, so you can ask a specific server directly:
# The new origin, bypassing DNS and the CDN
curl -s --resolve www.example.com:443:198.51.100.20 https://www.example.com/ | grep -o '<title>.*</title>'
# What a normal visitor gets, and which IP they actually reach
curl -sv -o /dev/null https://www.example.com/ 2>&1 | grep -iE 'connected to|^< (age|cache-control|cf-cache-status|x-cache)'
If the request pinned to the new origin returns the new content and the normal request returns the old, the difference is in between: DNS (if the normal request connected to the old IP) or a cache (if it connected to the CDN). If the request pinned to the new origin also returns old content, nothing is stale. The deploy didn’t happen, went to a different server, or the application has its own cache.
If your origin only accepts connections from the CDN’s IP ranges, run the pinned request from the origin itself against 127.0.0.1.
Step 3: read the cache headers
A cached response says so, if you know where to look:
$ curl -sI https://www.example.com/
HTTP/2 200
date: Wed, 07 Oct 2026 08:12:40 GMT
content-type: text/html; charset=utf-8
cache-control: public, max-age=3600
age: 2841
cf-cache-status: HIT
last-modified: Wed, 07 Oct 2026 07:20:02 GMT
Age is the standard header: how many seconds the response has been sitting in a cache. Here it’s been cached for 47 minutes, under a max-age of an hour, so this edge will keep serving it for about another 13 minutes unless you purge. An Age above zero means the copy came from a cache, whatever else the headers say.
Each CDN also reports hit or miss in its own header:
| CDN | Header | Values to look for |
|---|---|---|
| Cloudflare | cf-cache-status | HIT, MISS, EXPIRED, STALE, UPDATING, REVALIDATED, BYPASS, DYNAMIC |
| Amazon CloudFront | x-cache | Hit from cloudfront, Miss from cloudfront, RefreshHit from cloudfront |
| Fastly | x-cache, x-served-by | HIT or MISS, one per cache layer (MISS, HIT) |
| Vercel | x-vercel-cache | HIT, MISS, STALE, PRERENDER |
| Any cache following RFC 9211 | cache-status | e.g. ExampleCDN; hit |
Akamai doesn’t show cache status by default; it adds debug headers when the request includes Akamai’s Pragma debug values, such as akamai-x-cache-on.
Two values change the diagnosis:
cf-cache-status: DYNAMICmeans Cloudflare didn’t consider the response cacheable and fetched it from your origin. Cloudflare doesn’t cache HTML by default, so for most pages this is what you’ll see. If the content is old and the status isDYNAMIC, the CDN isn’t the cause; look at the origin or the browser.STALE(orstale-while-revalidate/stale-if-errorbehaviour in general) means the CDN served an expired copy on purpose, often because the origin failed to answer. That looks like “my change didn’t deploy” and is really “my origin is down”. Test the origin directly.
The HTTP check fetches the URL from outside your network and lists every response header, so you can see Age, cache-control and the CDN’s status without a terminal. It’s one vantage point: a CDN has many edges, and another edge can hold an older or newer copy.
A quick extra test: add a query string nobody has used, like ?nocache=8231. If that returns new content, a cache keyed on the full URL was serving the old copy. If it doesn’t, the cache may be ignoring query strings (a common CDN setting), or there’s no cache involved.
Step 4: rule out the browser
If curl gets the new version and your browser shows the old one, the stale copy is on your machine.
- HTTP cache. Open DevTools, Network tab, tick “Disable cache” and reload. The Size column shows
(memory cache)or(disk cache)for responses that never left the browser. - Service worker. Sites with offline support or PWA features can serve pages from a service worker’s cache. The Size column shows
(ServiceWorker). DevTools → Application → Service Workers lets you unregister it or bypass it for the network. - A cached redirect. Browsers keep
301redirects for a long time. If you removed a redirect and the browser still follows it, that’s why. A private window shows the real current behaviour.
A private window is the fastest check for all three. It starts without the normal profile’s cache, cookies or service workers.
Putting it together
| Authoritative DNS | Public resolvers | Pinned to new origin | Normal request | Stale layer |
|---|---|---|---|---|
| New | Some old | New | Old for some users | DNS. Wait for the old TTL. |
| New | All new (or all CDN) | New | Old, Age > 0, cache HIT | CDN or proxy cache. Purge. |
| New | All new (or all CDN) | Old | Old | Nothing is stale; the origin serves old content. Check the deploy. |
| New | All new (or all CDN) | New | New in curl, old in browser | Browser cache, service worker or cached redirect. |
| Old | Old | n/a | Old | The change was never published, or made in a zone that isn’t live. See the nameserver mismatch guide. |
Purge or wait
DNS: wait. You can’t purge other people’s resolvers. Cloudflare and Google offer pages to flush a single name from 1.1.1.1 and 8.8.8.8, which helps their users only. The old record’s TTL is the real limit, which is why the TTL guide recommends lowering it a day before a planned change.
CDN: purge, and purge narrowly. The CDN won’t refresh an object before its freshness runs out, and that can be days. Purge the URLs, prefixes or cache tags you changed rather than everything; “purge everything” sends every request to your origin at once, which is a bad thing to do to a struggling origin.
# Cloudflare: purge specific URLs
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"files":["https://www.example.com/","https://www.example.com/app.css"]}'
# CloudFront: invalidate paths
aws cloudfront create-invalidation --distribution-id E1EXAMPLE --paths "/" "/app.css"
Purges take a little while to reach every edge, and CloudFront charges for invalidation paths beyond a monthly free allowance. Check the response headers again afterwards: the first request should be a MISS, the next a HIT with a small Age.
Browsers: you can’t purge them. Whatever Cache-Control you sent is what visitors’ browsers obey. If you served app.js with max-age=31536000, returning visitors keep it for a year. The durable fix is in how you name and cache files:
- Put a content hash in static asset filenames (
app.3f9a1c.js) and cache those for a long time; a changed file gets a new name. - Serve HTML with a short
max-ageorCache-Control: no-cache, which means “revalidate before use”, not “don’t store”. Then a new deploy shows up on the next page load.
The caching and compression checklist covers headers in more detail.
When DNS and the CDN both changed
Moving a site onto a CDN, or moving the origin behind one, changes two layers in one go. Separate them:
- Moving onto a CDN changes your DNS record (to a CNAME or the CDN’s IPs). That part follows the old record’s TTL. Once traffic arrives, the CDN’s cache starts empty, so expect
MISSand a load spike at the origin first. - Moving the origin behind the same CDN usually doesn’t change public DNS. If the CDN reaches your origin by IP (Cloudflare’s proxied A record, for instance), the switch is immediate. If it reaches the origin by hostname, the CDN resolves that hostname itself and caches it under its TTL, so lower that TTL before the move.
- Changing CDN configuration (rules, origins, behaviours) has its own rollout to the edges. CloudFront shows a distribution as “Deploying” until that’s done. During it, different edges behave differently, which looks like propagation and isn’t DNS.
Test one layer at a time, with the commands above, before deciding what to purge or how long to wait.
Related
- DNS propagation delay: how long DNS changes take and how to check each resolver.
- TTL best practices: choosing TTLs and lowering them before a change.
- Nameserver mismatch: when the authoritative answer itself is wrong.
Check your own domain now
Free, no sign-up. Runs the exact check this guide describes and shows what to fix.