Available — Local Digital Products
HCA · Studio
TR — Contact ↗
Technical Note 11 · Deployment

You deployed, and visitors still see the old page

The file on the server is current; your browser shows the old one. A hard refresh fixes it, then a private window shows the old page again. The problem is not your browser cache but the layer in between — and the answer is not to hit purge every time.

Layer
Cloudflare
Symptom
Stale HTML
Evidence
cf-cache-status
Short answer

Cloudflare caches static assets (CSS, JS, images, fonts) by default and does not cache HTML. If your HTML is being cached, either a rule says cache everything or your server is sending a long Cache-Control for HTML. The right fix is to separate the two layers: version your static assets in the filename or query string so no purge is ever needed, because a new version is a new address. Keep HTML short-lived and purge only the changed addresses after a deploy. To diagnose, read the cf-cache-status header: HIT served from cache, MISS fetched from origin, DYNAMIC not cached at all.

Diagnose first

"The old page is showing" has at least three possible causes: the browser cache, the CDN cache, or the server's own cache. Rather than guessing, ask the headers:

curl -sSI https://example.com/ | grep -iE 'cf-cache-status|cache-control|age|last-modified'

What to read:

  • cf-cache-status: HIT — served from the CDN cache; your origin was never contacted
  • cf-cache-status: MISS — not in cache, fetched from origin
  • cf-cache-status: DYNAMIC — this address is not cached at all
  • age: 8412 — how many seconds the response has been sitting in cache

A HIT with a high age on HTML means you have found the problem. A DYNAMIC means the CDN is not involved and you should look at your origin instead.

Two separate layers

A static site has two kinds of content whose caching needs are opposites.

Assets (CSS, JS, images, fonts) change rarely, and when they change they change completely. These should stay in cache as long as possible.

HTML changes often and must be visible immediately when it does. It should either not be cached at all, or held for a very short time.

Most deployment problems come from applying one policy to both. "Cache everything" makes the site fast and every update invisible; "cache nothing" makes the CDN pointless.

Version your assets

For assets the right answer is not purging but changing the address. If the address changes whenever the content changes, the old copy sitting in cache stops mattering — nobody asks for it.

<link rel="stylesheet" href="/styles.min.css?v=20260918" />
<script src="/script.min.js?v=20260918" defer></script>

Stronger still is deriving the version string from a hash of the file contents, which makes forgetting to bump it impossible. Whichever method you use, the rule is the same: asset content must never change without the asset address changing.

Once that holds, a one-year cache lifetime on assets is completely safe and no post-deploy purge is needed for them.

Forgetting the version bump is the single weak point of this scheme. Update the CSS without changing the version and visitors get new HTML with old styles, so the page renders broken — and an ordinary purge will not fix it, because the browser is serving the file from its own cache.

The HTML side

There are two reasonable options for HTML. The first is not to cache it at all, leaving the CDN as a TLS-termination and protection layer. The second is a short lifetime — a few minutes, with revalidation.

On the origin side, a short lifetime looks like this:

Cache-Control: public, max-age=300, must-revalidate

That header tells browsers and the CDN the same thing, and at most five minutes after a deploy everyone sees the new content. If your pages already send ETag and Last-Modified, the check made when the lifetime expires is cheap: unchanged content is not downloaded again.

Purging after deploy

With versioned assets, purging is only needed for HTML — and it should be the final step of your deploy script, not something you remember to do by hand.

# purge only the addresses that changed
curl -sS -X POST \
  "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://example.com/","https://example.com/services/"]}'

Purging everything (purge_everything) is easy but blunt: all your assets drop out of cache too, the next visitors re-download everything, and your origin takes unnecessary load. On a small site you will not notice; on a busy one you will.

After purging, verify the result — again, do not assume:

curl -sSI https://example.com/ | grep -i cf-cache-status   # expect MISS

Traps

Development mode. Cloudflare's development mode disables caching temporarily and switches itself off after a few hours. Useful while debugging; a common cause of "the site got slow" when left on.

404 responses are cached too. If a page was deployed incompletely and returned 404, that 404 goes into the cache. Uploading the file afterwards is not enough — the address keeps returning 404 until you purge it specifically.

Order matters. Upload first, then purge. The other way round, the CDN re-caches the old content and nothing appears to have changed.

Origin headers steer the CDN. If you send a long Cache-Control for HTML, browsers will hold the file regardless of your CDN rules — and purging does not clear browser caches.

Summary

Seeing stale content after a deploy is not a cache malfunction; it is a missing policy. Version your assets and cache them for a long time; keep HTML short-lived and do a targeted purge as the last step of the deploy script. Once those two are in place, the whole "I forgot to purge" class of mistakes disappears — and understanding what happened always takes just one header: cf-cache-status.

Quick reference
Diagnosis
cf-cache-status: HIT from cache, MISS from origin, DYNAMIC not cached
Default
Cloudflare caches static assets; it does not normally cache HTML
Assets
Version them (?v= or a content hash) — no purge needed
HTML
max-age=300, must-revalidate, or do not cache at all
Purge
Should be the last step of the deploy script, not a manual habit
Targeted purge
Prefer {"files":[...]}; purge_everything is a blunt tool
Order
Upload first, then purge — the reverse re-caches the old content
Overlooked
404 responses are cached too; purge that address after fixing it
Frequently Asked Questions

About Cloudflare and deployment.

I uploaded the file but the site shows the old version. Where do I start?

Stop guessing and read the headers: curl -sSI and look at cf-cache-status, age and cache-control. A HIT with a high age means the response came from the CDN cache. A DYNAMIC means the CDN is not involved and the problem is at the origin or in your browser. That one command separates the three possibilities.

Is it bad to run purge_everything on every deploy?

It works, but it is a blunt tool. All your assets drop out of cache as well, so the next visitors re-download everything from CSS to images and your origin takes unnecessary load. If your assets are versioned they never need purging in the first place; target only the HTML addresses that changed.

Why is versioning assets better than purging them?

Because it removes the race condition. When a versioned address changes, the new file is a new address; nobody requests the old copy still sitting in cache, so it never needs clearing. That is what makes a one-year cache lifetime safe, and it reduces deployment to a single variable: remember to bump the version.

I fixed the page but it still returns 404.

404 responses are cached too. Once an incompletely deployed address has returned 404, uploading the file later is not enough — you have to purge that address specifically. Verify new addresses after a deploy, not just the homepage.

Availability and Quotes

Have an idea?
Half a sentence is enough.