# Cloudflare Pages serves stale HTML after every deploy

> A Cache Rule makes Cloudflare Pages edge-cache HTML, so your next deploy stays invisible at the canonical URLs for up to 24h — until you purge.

**Canonical URL**: https://agentcookbooks.com/blog/cloudflare-pages-purge-cache-after-deploy/

**Published**: 2026-05-29

**Tags**: claude-code, deployment

---

I shipped three commits of SEO fixes, every build went green, and the live pages kept serving the old HTML. Not for a few seconds — for hours, and they would have kept doing it for most of a day. The cause was the HTML edge-cache I'd added on purpose a few weeks earlier. A Cloudflare Pages deploy updates the origin; it does not purge the zone's edge cache. The [post that told me to add that Cache Rule](/blog/cloudflare-pages-html-cache-the-missing-rule/) even said the edge would serve cached HTML "until the TTL expires or a deploy invalidates the cache." The second half of that sentence is wrong.

## What I shipped

Three commits to `main` — visible breadcrumbs, trimmed `<title>` tags, `og:image:width/height`, a dropped `SoftwareApplication` JSON-LD node, a category-grouped skills index. All verified in the local `dist/` build before pushing. Cloudflare Pages auto-deploys on push; all three builds went green within a couple of minutes.

Then I curled the live canonical URLs to confirm, and got nothing I'd just shipped:

```
$ curl -s https://agentcookbooks.com/skills/systematic-debugging/ \
  | grep -c SoftwareApplication
1                       # removed two commits ago — should be 0

$ curl -s https://agentcookbooks.com/skills/systematic-debugging/ \
  | grep -oE '"@type":"ListItem"' | wc -l
2                       # the new 4-level breadcrumb wasn't there
```

Old title on the trimmed posts, no `og:image:width`, no breadcrumb `<nav>`. Every one of the three commits was missing — but `dist/` was correct and the deploy was green.

## The header that explained it

One `curl -sI` pointed straight at the cause:

```
$ curl -sI https://agentcookbooks.com/skills/systematic-debugging/ \
  | grep -iE 'cf-cache-status|age'
cf-cache-status: HIT
Age: 32963
```

`HIT`, and the cached copy was 32,963 seconds old — about 9.15 hours. The Cache Rule I'd set up makes HTML edge-eligible and honors the `_headers` directive `Cache-Control: public, max-age=3600, s-maxage=86400`, which is a 24-hour edge TTL (`s-maxage`). The deploy had replaced the origin, but the edge was still inside that 24-hour window and kept handing out the pre-deploy HTML. With `Age` at 32,963 against an `s-maxage` of 86,400, there were still about 14.8 hours before it would refresh on its own.

## Proving it was the edge, not the build

The clean way to separate "deployed wrong" from "deployed fine but stale at the edge" is a cache-buster: append a unique query string so the URL isn't in the cache, forcing a `MISS` and an origin fetch.

```
$ curl -s "https://agentcookbooks.com/skills/systematic-debugging/?cb=$(date +%s)" \
  | grep -c SoftwareApplication
0                       # origin is correct

$ curl -s ".../skills/systematic-debugging/?cb=$(date +%s)" \
  | grep -oE '"@type":"ListItem"' | wc -l
4                       # the new breadcrumb tier is there

$ curl -s ".../blog/cloudflare-pages-html-cache-week-one-449-visitors/?cb=$(date +%s)" \
  | grep -oE '<title>[^<]*</title>'
<title>Week one on Cloudflare Pages: 449 visitors, 5.99% cached</title>
```

Six change-signals, all stale at the bare canonical URL, all fresh at the `?cb=` URL. That's conclusive: the build was right, the origin was right, and the only thing wrong was that the edge was serving a snapshot from nine hours ago.

## The fix, and why the deploy didn't do it

Cloudflare dashboard → the zone → **Caching → Configuration → Purge Cache → Purge Everything** (or purge the specific changed URLs). The next request is a `MISS`, the edge re-fetches the new deployment, and the canonical URLs go fresh. The alternative is to wait out the `s-maxage` window.

The trap is that a Pages *deployment* and the zone's *Cache Rule* are two systems sharing one dashboard. The deployment is immutable-per-version and updates what the origin serves. The Cache Rule caches by URL at the edge and has no idea a deployment happened — nothing in the Pages deploy pipeline issues a purge against it. So the more correct your caching setup is, the longer your deploys stay invisible.

This matters more for SEO than for humans. A returning visitor might shrug at a few stale hours. Googlebot re-crawls on its own schedule, and if it crawls inside the stale window it indexes the *old* page — so the entire point of an SEO deploy (the new titles, breadcrumbs, schema) is invisible to the crawler for up to a day, exactly when you most want it seen.

## What I'd change

Two moves, both cheap:

1. **Add a purge to the deploy ritual.** Once an HTML Cache Rule exists, "push to `main`" is no longer the last step — "purge" is. On this site that's a manual dashboard click; a project with a Cloudflare API token can fire a purge from CI right after the Pages build reports success.
2. **Keep the cache-buster in the back pocket.** `?cb=$(date +%s)` is the fastest "is it deployed or just stale?" test there is — no purge, no waiting, no guessing. If the `?cb=` URL is fresh and the bare URL is stale, you don't have a build problem, you have a cache problem, and the fix is a purge, not another commit.