Cache Purging & Invalidation
Cache purging is the controlled removal of stale objects from a CDN’s edge so the next request re-fetches from origin — and choosing the right granularity (everything, one URL, a prefix, or a tag) is the difference between a clean deploy and a thundering herd against your origin.
Key implementation points:
- Always purge at the narrowest granularity that covers the change — single-URL or tag purge over purge-everything — to protect origin from a cold-cache stampede.
- Group related objects with cache tags / surrogate keys at response time so one purge call invalidates dozens of URLs atomically.
- Prefer soft purge (mark stale, revalidate in background) over hard purge wherever the platform supports it, to avoid serving slow uncached responses during revalidation.
- Wire purge into CI/CD on deploy so the cache is invalidated the instant new origin content ships, and make the call idempotent and retried.
This guide covers how invalidation actually works on the wire, the practical differences between purge scopes, and how each major CDN exposes them. It is part of the broader CDN Caching & Performance Optimization topic and pairs closely with how you structure your cache key and Vary configuration — because a purge can only target objects whose identity it can describe.
How invalidation works at the edge
A CDN caches each response under a cache key — typically a hash of the host, path, query string, and any headers named in Vary or in a custom cache-key rule. Purging does not delete a file from a single disk; it broadcasts an instruction to every edge node telling it that objects matching some predicate are no longer valid. Each point of presence (POP) processes that instruction independently, which is why purge timing is eventually consistent rather than instantaneous.
There are two fundamentally different ways an edge can honour a purge:
- Hard purge (evict): the object is removed from the cache. The next request is a guaranteed
MISSand blocks on origin until the response is fetched. Simple, but it exposes origin to a stampede if many clients hit the same purged URL simultaneously. - Soft purge (mark stale): the object stays in cache but is flagged stale. The next request can be served immediately from the stale copy while the edge revalidates against origin in the background. This is the same machinery behind stale-while-revalidate and resilient caching, applied to a manual invalidation rather than an expiry.
The predicate you purge against determines the scope:
| Scope | What it matches | Typical use | Risk |
|---|---|---|---|
| Single URL | One exact cache key (host + path + query) | Publishing one article or image | Misses query-string variants |
| Prefix / path | All objects under a path | A whole section redeployed | Can over-purge a busy directory |
| Tag / surrogate key | Objects tagged at response time | Invalidate “product 42” across pages | Requires tagging discipline |
| Host | Everything for a hostname | Subdomain-wide change | Cold cache for that host |
| Everything | The entire zone | Emergency / full redeploy | Origin stampede, slow recovery |
Why granularity matters
The cost of over-purging is paid by your origin. If you purge an entire zone serving 50,000 hot objects, every one of those becomes a MISS and the next wave of traffic funnels to origin simultaneously. On a high-traffic property this looks identical to a traffic spike or an outage. Narrow purges keep your hit ratio high (see customizing cache keys to improve hit ratio) and keep origin load predictable. Reach for purge-everything only when you cannot describe the change any other way.
Cache tags and surrogate keys
The single most useful invalidation primitive is the cache tag (Cloudflare’s term) or surrogate key (Fastly’s term). When origin emits a response it attaches one or more tags via a header. The edge records the association between the cached object and each tag. Later you purge by tag, and every object carrying that tag is invalidated in one call — regardless of how many distinct URLs they live at.
A product page, a category listing, a sitemap, and an API endpoint might all depend on “product 42”. Tag all four responses with product-42 at origin:
# Fastly origin response header
Surrogate-Key: product-42 category-shoes inventory
# Cloudflare origin response header (Enterprise)
Cache-Tag: product-42,category-shoes,inventory
Now a single tag purge of product-42 clears every page that referenced it, with no need to enumerate URLs. This is the right model for content with many-to-many relationships — exactly the case where single-URL purging falls apart. Design your tags around data ownership, not page layout: tag by the entity that changed (order-9981, user-settings:tenant-7) so that whoever mutates the data can purge without knowing which pages render it.
Provider-specific implementation
Cloudflare
Cloudflare exposes purge through the zone purge endpoint. Free and Pro plans get purge-everything and purge-by-URL; purge by tag, host, or prefix requires Enterprise. URL purges must include the scheme and any query string that is part of the cache key.
# Purge specific URLs (all plans)
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"files":["https://www.example.com/app.css","https://www.example.com/app.js"]}'
# Purge by cache tag (Enterprise)
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"tags":["product-42","category-shoes"]}'
# Purge by prefix (Enterprise)
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"prefixes":["www.example.com/blog/2026/"]}'
The API token needs the Cache Purge permission scoped to the zone. A successful call returns {"success":true,"result":{"id":"<zone_id>"}} within a few hundred milliseconds; the actual edge-wide invalidation completes shortly after. Cloudflare purges are hard purges — there is no built-in soft-purge flag, so pair them with stale-while-revalidate in your Cache-Control if you want background revalidation. For a deploy-time recipe with retries and a GitHub Actions wiring, see purging Cloudflare cache via API on deploy.
AWS CloudFront
CloudFront calls invalidation CreateInvalidation and works on path patterns, not exact URLs or tags. Patterns may use a trailing * wildcard. There is a free tier of 1,000 invalidation paths per month; beyond that each path costs money, and a /* wildcard counts as a single path regardless of how many objects it matches.
aws cloudfront create-invalidation \
--distribution-id E123ABCDEXAMPLE \
--invalidation-batch '{
"Paths": { "Quantity": 2, "Items": ["/app.css", "/blog/2026/*"] },
"CallerReference": "deploy-2026-06-20T12-00-00Z"
}'
CallerReference must be unique per request — use the commit SHA or an ISO timestamp so retries don’t collide. The call returns an invalidation Id with Status: InProgress; poll get-invalidation until Status: Completed, which typically takes 60–300 seconds. CloudFront has no cache-tag concept, so model grouped invalidation through path structure: lay out URLs so that everything depending on one entity lives under a shared prefix you can wildcard. CloudFront only invalidates the viewer-facing cache; if you front it with an origin or a regional edge cache, account for those layers separately.
Fastly
Fastly’s purge is instant (sub-150ms globally) and is the most flexible of the three. It supports purge by URL, by surrogate key, soft purge, and purge-all, all through a simple API.
# Purge a single URL (hard)
curl -X PURGE "https://www.example.com/app.css"
# Purge by surrogate key
curl -X POST "https://api.fastly.com/service/$SERVICE_ID/purge/product-42" \
-H "Fastly-Key: $FASTLY_TOKEN"
# Soft purge by surrogate key (mark stale, serve while revalidating)
curl -X POST "https://api.fastly.com/service/$SERVICE_ID/purge/product-42" \
-H "Fastly-Key: $FASTLY_TOKEN" \
-H "Fastly-Soft-Purge: 1"
# Purge everything for the service
curl -X POST "https://api.fastly.com/service/$SERVICE_ID/purge_all" \
-H "Fastly-Key: $FASTLY_TOKEN"
The Fastly-Soft-Purge: 1 header is the key differentiator: combined with stale-while-revalidate (or Fastly’s stale-if-error) in your VCL or Cache-Control, a soft purge means clients keep getting fast responses while the edge fetches fresh content out of band. For grouped invalidation, emit Surrogate-Key headers from origin and purge keys — there is no monthly limit and no per-purge cost, which makes Fastly’s model practical for high-frequency, fine-grained invalidation.
Azure Front Door and others
Azure Front Door purges by path (az afd endpoint purge --content-paths "/blog/*") and supports wildcard sub-paths; it has no tag concept. Akamai offers fast purge by URL, by CP code, and by cache tag with both invalidate (revalidate on next hit) and delete (hard evict) semantics. The pattern across all of them is the same: identify whether the provider supports tags, whether purge is soft or hard, and what the propagation envelope is.
Platform comparison
| Provider | Granularity | Wire behavior | Failover / Notes |
|---|---|---|---|
| Cloudflare | URL (all plans); tag / prefix / host (Enterprise) | Hard purge; propagates in seconds | No native soft purge — use stale-while-revalidate |
| AWS CloudFront | Path pattern with * wildcard |
Hard purge; 60–300s; 1,000 free paths/mo | No tags; unique CallerReference per call |
| Fastly | URL, surrogate key, purge-all | Instant (<150ms); soft or hard | Fastly-Soft-Purge: 1; no purge limits or cost |
| Azure Front Door | Path / wildcard | Hard purge; propagation in minutes | No tags; purge per endpoint |
| Akamai | URL, CP code, cache tag | Invalidate or delete; fast purge | Invalidate = revalidate on next hit |
Step-by-step: integrating purge into CI/CD on deploy
The goal is for the cache to be invalidated the moment new origin content goes live, with no manual step and no race where the CDN serves old content against a new origin.
-
Decide the purge scope from the deploy diff. If you can compute the list of changed asset URLs (e.g. from a build manifest), purge exactly those. If the change touches a known entity, purge its tag. Fall back to a prefix only when the diff is too broad to enumerate.
-
Order the steps correctly. Deploy origin first, confirm the new version is serving, then purge. Purging before origin is updated re-caches the old content and defeats the deploy.
-
Issue the purge call with retries. Treat purge as idempotent and retry on 5xx and rate-limit (429) responses with exponential backoff.
#!/usr/bin/env bash set -euo pipefail urls_json='{"files":["https://www.example.com/app.css","https://www.example.com/app.js"]}' for attempt in 1 2 3 4 5; do code=$(curl -s -o /tmp/purge.out -w '%{http_code}' \ -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -H "Content-Type: application/json" --data "$urls_json") if [ "$code" = "200" ]; then echo "purge ok"; exit 0; fi echo "purge attempt $attempt failed (HTTP $code), retrying..." sleep $((2 ** attempt)) done echo "purge failed after retries"; cat /tmp/purge.out; exit 1 -
Verify the invalidation took effect. Re-request a purged URL and assert a fresh response. On Cloudflare check the
CF-Cache-Statusheader forMISSorEXPIREDon the first post-purge hit; on CloudFront checkX-Cache: Miss from cloudfront; on Fastly checkX-Cache: MISS.curl -sSI "https://www.example.com/app.css" | grep -i 'cf-cache-status\|x-cache\|age' -
Fail the deploy if verification fails. A purge that silently no-ops (wrong zone ID, URL not matching the cache key, expired token) is worse than no purge because you believe you shipped. Make the verify step gate the pipeline.
TTL, caching, and propagation implications
Purging interacts with your TTLs in ways worth thinking through. A short edge TTL makes purging less critical — content self-expires quickly — but increases origin load, the same trade-off covered in mastering TTL strategies for DNS. A long edge TTL makes purging essential, because without it stale content can persist for hours.
Propagation is eventually consistent. Most edges clear within a second or two, but a slow or partitioned POP can lag. During that window different users may see different versions. If version-skew between assets is dangerous (a CSS file referencing classes a new HTML file no longer emits), prefer immutable, content-hashed filenames (app.4f3a1c.css) over purging at all: a new build references new URLs, the old ones simply age out, and there is no skew window. Reserve purging for HTML and API responses that cannot carry a content hash in their URL.
Browser caches sit downstream of the CDN and a CDN purge does not touch them. If you set a long max-age on the client, a purge only refreshes the edge; returning visitors keep their local copy until it expires. Use a short client max-age with a longer s-maxage for the shared edge cache so that purges are meaningful end to end.
Troubleshooting and rollback
| Symptom | Likely cause | Fix |
|---|---|---|
| Still stale after purge | URL purged doesn’t match the cache key (missing query string, wrong scheme, trailing slash) | Inspect the real cache key; purge the exact normalized URL or use a tag/prefix |
| Still stale after purge | Browser cache, not edge | Check Age and CF-Cache-Status; lower client max-age; hard-reload to confirm |
| Still stale in one region | Slow POP propagation | Wait the propagation envelope; re-issue purge; check provider status |
| Origin overloaded after purge | Purged too broadly (purge-everything) | Switch to tag/URL purge; enable stale-while-revalidate to absorb the stampede |
| Purge “succeeds” but nothing changes | Wrong zone/distribution ID or expired token | Verify credentials and IDs; assert CF-Cache-Status: MISS post-purge |
| 429 on purge | Rate limit during a deploy storm | Batch URLs into fewer calls; back off and retry |
Rollback protocol when a bad asset was deployed and cached:
- Redeploy the previous-good origin version (or roll back the object in storage).
- Confirm origin now serves the good version with a direct origin request that bypasses the CDN.
- Purge the affected URLs or tag — narrowly, not everything.
- Verify
CF-Cache-Status: MISSthen a subsequentHITreturning the good content. - If origin cannot be rolled back fast, purge to force a
MISSonly if origin definitely serves good content; otherwise leave the stale-but-safe copy and fix origin first.
Edge cases and gotchas
- Query-string sensitivity: if your cache key includes query parameters, a single-URL purge of
/pagewill not clear/page?utm=x. Purge by prefix or tag, or normalize query strings in your cache-key rules first. - Trailing slashes and scheme:
http://vshttps://and/pathvs/path/are distinct cache keys on most CDNs. Purge the canonical form your edge actually stored. - Vary explosion: content varying on
Accept-EncodingorAccept-Languageproduces multiple cached variants per URL. A URL purge usually clears all variants, but confirm — and keep yourVaryminimal. - Tiered caching: Cloudflare Tiered Cache, CloudFront origin shield, and Fastly shielding add an intermediate cache layer. Purges propagate to these, but verify the shield cleared, not just the outer edge.
- CallerReference reuse (CloudFront): reusing a reference returns the original invalidation result and silently skips your new one. Always generate a fresh reference.
- Purge-all rate limits: some providers throttle full purges. Don’t script purge-everything into a hot deploy loop.
- Soft purge with no
stale-while-revalidate: a soft purge without a stale directive behaves much like a hard purge — the stale window is zero. The two features must be configured together.
Frequently Asked Questions
Should I purge a single URL or purge everything on deploy? Purge the narrowest scope that covers your change. Single-URL or tag purges keep your cache warm and protect origin from a stampede, whereas purge-everything turns every hot object into a simultaneous miss. Reserve purge-everything for emergencies or when you genuinely cannot enumerate or tag what changed.
What is the difference between soft purge and hard purge?
A hard purge evicts the object so the next request is a guaranteed miss that blocks on origin. A soft purge marks the object stale but keeps it, so the edge can serve the stale copy immediately while revalidating in the background. Soft purge is gentler on origin and on user-facing latency, but it only works when paired with a stale-while-revalidate directive.
How long does a purge take to propagate globally? It depends on the provider: Fastly is effectively instant (under 150ms), Cloudflare clears within seconds, and CloudFront typically takes 60 to 300 seconds. Propagation is eventually consistent, so a slow or partitioned POP can briefly serve old content. Verify by re-requesting the URL and checking the cache-status header rather than assuming the call’s success response means the edge is clear everywhere.
Why is content still stale even though my purge returned success?
The most common cause is that the URL you purged does not match the stored cache key — a missing query string, wrong scheme, or trailing-slash mismatch. The next most common is a downstream browser cache that the CDN purge never touched. Inspect the actual cache key and the Age and cache-status headers; purge by tag or prefix when exact URLs are hard to reproduce.
Can I avoid purging entirely? For static assets, yes — use immutable, content-hashed filenames so each build references new URLs and old ones age out naturally, eliminating version skew. Purging remains necessary for HTML and API responses that cannot carry a hash in their URL, where tag-based invalidation is the cleanest approach.