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

Which files need invalidating after a deploy Four categories. Hashed assets are immutable and never need purging. HTML with a short TTL expires on its own within a minute. Unhashed assets such as favicon or a logo do need purging when they change. Redirect and header files usually apply on deploy without a purge. Most of a static deploy needs no purge at all No purge needed hashed CSS and JS — immutable hashed images and fonts new filenames, old ones simply age out Expires on its own HTML with max-age=0, revalidated sitemap.xml, feeds stale for seconds, not worth a purge Purge these paths unhashed assets that changed long-cached HTML you edited Never purge everything every edge refetches at once origin spike right after a change If the top-left box covers most of your output, your cache policy is already doing the work
The green box is the goal: a build where nothing needs invalidating because nothing was overwritten. Hashed filenames are what make a deploy and a purge independent.

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.

Origin load after three purge strategies Three lines showing origin requests per second in the two minutes after a deploy. Purging everything spikes to 1,850 requests per second. Purging by tag for one section peaks at 240. Purging only changed paths peaks at 60, barely above the baseline of 35. Origin requests per second after the deploy 2,000 0 purge everything · 1,850/s purge by tag · 240/s purge changed paths · 60/s deploy +30 s +120 s Same site, same traffic, same deploy · baseline origin load is 35 requests per second
The red line is not a graph of cache invalidation; it is a graph of a site attacking its own origin. Everything it refetched was already correct.

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.

Hard purge versus soft purge at the edge With a hard purge the object is deleted, so the next reader waits for a full origin fetch. With a soft purge the object is marked stale, so the next reader is served the stale copy immediately while the edge revalidates in the background and the following reader gets fresh content. Who waits for the origin Hard purge object deleted next reader waits ~180 ms fresh, cached again Soft purge object marked stale next reader served instantly revalidation happens behind them fresh, cached again The same effect is available without a purge API by serving stale-while-revalidate in Cache-Control
A soft purge costs one reader a slightly stale page and saves every reader the origin round trip. On content sites that trade is almost always worth taking.

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:

StrategyPurge callsOrigin peakTime to steady stateStale window
Purge everything11,850 req/s2 m 10 s0 s
Purge by tag (one section)1240 req/s35 s0 s
Purge changed paths only360 req/s8 s0 s
No purge (hashed assets, short-TTL HTML)041 req/sup 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.