A new Pages deploy showed Success and was marked Production, yet the custom domain and <project>.pages.dev both kept serving the old index.html for over a day. I answered that thread on Cloudflare Community on Sep 28. Cloudflare Pages not updating after deploy on both hostnames at once points at a header, not at the deploy.
The headers in the post said it all: cf-cache-status: HIT, an age of about 30 hours, and cache-control: public, s-maxage=604800. Two more redeploys with different content hashes didn't change a thing.
Why is Cloudflare Pages still serving the old version?
Because something told the CDN it could keep that HTML for a week. The Pages default for a cacheable asset is public, max-age=0, must-revalidate, and Pages never adds s-maxage by itself. A s-maxage=604800 comes from the project: a _headers rule, a Pages Function, or an advanced mode _worker.js.
You can see the default on any Pages site that doesn't override it. I run two Pages projects, and on 2026-09-30 both returned the same thing on the custom domain and on pages.dev:
curl -sI https://<project>.pages.dev/ | grep -iE "cache-control|cf-cache-status|^age"
cache-control: public, max-age=0, must-revalidate
cf-cache-status: DYNAMIC
That's what "no custom caching" looks like. The Headers section of the Serving Pages page lists it under headers sometimes added, for any request without an Authorization or Range header.
Where the s-maxage usually comes from
The most likely source is a _headers file in the build output with a catch-all rule, the kind that gets copied from a performance guide:
/*
Cache-Control: public, s-maxage=604800
That rule matches every path, HTML included. Search the build output as well as the source, because frameworks copy public/ into it:
grep -rn -i "s-maxage" dist/ public/ functions/ _worker.js 2>/dev/null
The _headers page has a caution worth knowing here. Rules in _headers don't apply to responses generated by Pages Functions. If an SSR framework or _worker.js renders the page, the header is being set in that code's Response, and the grep above will find it in functions/ or the worker file.
How to purge the Pages cache without a zone
You mostly can't, and that's the uncomfortable part. The Serving Pages page describes one purge path: go to your zone and use Caching, Configuration, Purge Everything. A project on pages.dev with a custom domain on some other DNS provider has no zone in the account to purge.
The same page's Asset retention section says assets are cached per data center with a one-week TTL, and after a new deploy the old ones can stay up to a week. That matches the thread. The redeploys didn't evict HTML cached under the project's own s-maxage.
So the fix is two steps, and the order matters:
- Remove the rule that sets
s-maxageon HTML and deploy. Otherwise every future deploy starts a fresh seven-day clock. - Wait out the copies already cached. Worst case that's the week the header promised.
If the custom domain is on a zone in your account, Purge Everything on that zone clears it there. The pages.dev hostname isn't part of your zone, so it can stay stale until retention runs out.
Check the new build on its deployment URL
Every deployment also gets its own address. The preview deployments page says it's hash-based, like <hash>.<project>.pages.dev, and atomic, so it keeps pointing at that exact build. Open it from the deployments list in the dashboard.
It's a different hostname, so it shouldn't share the cached HTML from the production hostnames. I haven't tested that part myself. It's still the quickest way to prove the new build is correct while you wait, instead of redeploying a third time.
Cache Rules on a custom domain cause the same symptom
Even with clean headers, a custom domain can go stale if its zone caches HTML. The Serving Pages page says it plainly: adding caching to your custom domain may lead to stale assets after a deployment, and it can also break Pages redirects and Functions.
If the custom domain sits on your zone, read its Cache Rules. Both of my Pages projects sit on custom domains in my own zone, and DYNAMIC is what those custom domains return: the HTML isn't being held at the edge. The read below uses the same cf() helper as my Cloudflare zone security checklist:
cf /zones/$ZONE_ID/rulesets/phases/http_request_cache_settings/entrypoint \
| jq '.result.rules[] | {description, expression, action_parameters}'
A rule that sets an Edge TTL on everything, or on /, is the same bug in a different place.
Long cache headers that are safe on Pages
Long caching is fine for files whose names change when their content does. The _headers page's own example scopes it to one folder:
/static/*
Cache-Control: public, max-age=31556952, immutable
Put fingerprinted files there and leave HTML on the default. Bundlers like Vite and Astro already put hashes in asset names, so the only thing to avoid is a /* rule. HTML is the file that has to change on every deploy, and it's the one that must never get a week-long shared cache.
A purge button for Pages projects without a zone would be a fair feature request. I couldn't find one in the docs.