Purging the CDN Cache After a Static Deploy
The reflex after a deploy is to purge the cache. On a well-configured static site that reflex is usually wrong: hashed assets are immutable and never need invalidating, and HTML is either short-cached or revalidated. A blanket purge throws away every cached object at every edge location and sends the entire next wave of traffic to the origin.
This guide covers what genuinely needs purging, how to do it by path or tag rather than wholesale, and how to wire it into a deploy so it runs at the right moment. It is the invalidation half of CDN Caching Rules for SSGs.
Prerequisites
- A deploy that produces a list of changed files — most CI systems can diff the build output against the previous artifact.
- API credentials for your CDN with purge permissions, stored as a CI secret.
- A cache policy where assets are content-hashed and immutable; if not, start there.
What Actually Needs Purging
Purge by Path, From the Diff
The deploy already knows which files changed. Turn that into a purge list rather than purging by intuition:
#!/usr/bin/env bash
set -euo pipefail
# Compare the new build against the previously deployed artifact
CHANGED=$(diff -rq prev-dist dist 2>/dev/null \
| awk '/^Files .* differ$/ {print $2}; /^Only in dist/ {gsub(/:/,"",$3); print $3"/"$4}' \
| sed 's|^dist||' | grep -vE '/_assets/.*\.[0-9a-f]{8}\.' || true)
# Nothing but hashed assets changed → nothing to purge
[ -z "$CHANGED" ] && { echo "purge: nothing to invalidate"; exit 0; }
echo "$CHANGED" | while read -r p; do echo "https://example.com$p"; done > purge-urls.txt
wc -l < purge-urls.txt
Then send it in batches — providers cap the number of URLs per call, typically at 30 to 500:
split -l 30 purge-urls.txt batch-
for f in batch-*; do
jq -Rn --args '{files: $ARGS.positional}' $(cat "$f") \
| curl -sS -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" -H 'Content-Type: application/json' --data @-
sleep 1
done
The grep -v on hashed paths is the important line: without it, every deploy purges hundreds of asset URLs that did not need it, which is the slow version of purging everything.
Purge by Tag for Template Changes
Path purging breaks down when a template change touches every page in a section. Cache tags solve this: attach a label at the edge, then purge the label.
// Worker: tag responses by section so a template change purges one group
export default {
async fetch(request, env, ctx) {
const res = await env.ASSETS.fetch(request);
const section = new URL(request.url).pathname.split('/')[1] || 'home';
const out = new Response(res.body, res);
out.headers.set('cache-tag', `section-${section},site`);
return out;
},
};
# One call invalidates every page in the guides section
curl -sS -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" -H 'Content-Type: application/json' \
--data '{"tags":["section-guides"]}'
Tagging is worth the setup on any site where template changes are routine. It turns "purge 1,200 URLs in 40 batched calls" into one call, and it makes the intent legible in the deploy log.
Run It After the Deploy, Never Before
Ordering is the failure that produces the most confusing symptoms. Purge before the new build is live and the edge refetches the old content and caches it again — now with a fresh TTL. The site is stale for longer than if you had done nothing.
# .github/workflows/deploy.yml (excerpt)
- name: Deploy
run: npx wrangler deploy
- name: Verify the new build is live
run: |
for i in $(seq 1 20); do
LIVE=$(curl -s https://example.com/ | grep -o 'content="[a-f0-9]\{7\}"' | head -1)
[ "$LIVE" = "content=\"${GITHUB_SHA:0:7}\"" ] && break
sleep 3
done
- name: Purge changed paths
run: ./scripts/purge.sh
The verification step is what makes the ordering reliable rather than hopeful: it checks the build stamp described in Rollbacks and Deploy Safety for Static Sites before invalidating anything.
Soft Purge Where It Is Available
Several providers offer a gentler operation than eviction: mark an object stale rather than deleting it. The edge then serves the stale copy while it revalidates in the background, so readers never wait for the origin and the origin never sees a stampede.
If your provider has no soft-purge API, stale-while-revalidate in the response headers gets you most of the same behaviour without any purge call at all — the edge serves the cached copy and refreshes it in the background once it passes its freshness window. Combining a short max-age with a generous stale-while-revalidate means routine deploys need no invalidation step whatsoever, which is the least error-prone configuration available.
Measured Impact
A documentation site with 1,600 pages, 380 assets and roughly 40 requests per second of steady traffic, measured across ten deploys of each strategy:
| Strategy | Purge calls | Origin peak | Time to steady state | Stale window |
|---|---|---|---|---|
| Purge everything | 1 | 1,850 req/s | 2 m 10 s | 0 s |
| Purge by tag (one section) | 1 | 240 req/s | 35 s | 0 s |
| Purge changed paths only | 3 | 60 req/s | 8 s | 0 s |
| No purge (hashed assets, short-TTL HTML) | 0 | 41 req/s | — | up to 60 s |
The last row deserves consideration rather than dismissal. On a site whose HTML carries max-age=0, must-revalidate, skipping the purge entirely costs a revalidation round trip per page and no staleness at all — the edge asks the origin whether its copy is current, and gets a cheap 304 when it is. That is a perfectly good default, and it is why the invalidation strategy is really a consequence of the caching policy rather than a separate decision.
Verify Rather Than Assume
A purge API returning success means the request was accepted, not that every edge location has acted on it. Confirm by fetching through the edge and reading the cache status header:
curl -sI https://example.com/guides/deploying/ | grep -iE 'cf-cache-status|age|x-cache'
An age value that resets to zero and a MISS on the first request after a purge is what completion looks like. Checking two or three representative paths from different regions — a CI runner and a laptop are usually enough — catches the case where a purge succeeded in one region and is still propagating in another.
Pitfalls & Rollback
- Purging everything by default. It is the option most likely to cause an incident and least likely to be necessary.
- Purging before the deploy completes. The edge re-caches the old build with a fresh TTL.
- Purging hashed assets. They are immutable; the URL changed, so nothing is stale.
- Assuming the API response means propagation. Purges are eventually consistent; verify by fetching, not by trusting a 200.
- Unbatched purge lists. Providers cap URLs per call and rate-limit; a loop of single-URL calls will be throttled halfway through.
- Rollback: purging is not reversible, but it is not destructive either — the worst case is a period of higher origin load. If a purge script misbehaves, disable the CI step; the caching policy alone keeps the site correct.
Conclusion
On a static site with hashed assets and short-TTL HTML, the correct purge after most deploys is no purge at all. When invalidation is genuinely needed, derive the list from the build diff, exclude hashed paths, batch the calls, and run it only after verifying the new build is live. Reach for cache tags when template changes routinely touch whole sections. The caching policy that makes all of this cheap is in CDN Caching Rules for SSGs.
FAQ
Do I need to purge at all if my assets are hashed?
Rarely. Hashed assets are immutable, so nothing to invalidate, and HTML is normally served with a short TTL or revalidation. A purge is only needed for unhashed files that changed, or when HTML is cached long at the edge.
Why is purging everything harmful?
Because every edge location loses every object at once and refetches from the origin on the next request. On a busy site that is a self-inflicted traffic spike at exactly the moment you have just changed something, which is also the worst moment to be debugging origin load.
What is cache tagging and when should I use it?
Tagging attaches labels to responses at the edge so you can purge a group in one call — for example every page in a section. It is worth setting up on sites where a template change affects a known subset of pages, because it turns thousands of path purges into one tag purge.
How long does a purge take to propagate?
Seconds to a couple of minutes depending on the provider and the number of paths. Treat it as eventually consistent, and never assume a purge has completed just because the API returned success.
Should the purge run before or after the deploy?
After, always. Purging before the new content is live invites the edge to refetch and re-cache the old version, which leaves you worse off than not purging at all.
Related
- Parent: CDN Caching Rules for SSGs — the policy that decides whether you need this at all.
- Setting Cache-Control Headers on Cloudflare Pages — the headers this depends on.
- Setting Up Proper Cache Headers on Netlify — the same policy on another host.
- Rollbacks and Deploy Safety for Static Sites — the build stamp the purge step verifies.
- Atomic Deploys vs Incremental Uploads — why nothing is overwritten in the first place.