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.
Purge propagation across edge POPs A single purge API call fans out from a control plane to regional edge points of presence, each clearing matching objects at slightly different times, after which requests miss and revalidate from origin. Purge API call URL | prefix | tag Control plane POP: EU cleared ~40ms POP: US cleared ~60ms POP: APAC cleared ~300ms Next request = MISS, revalidate from origin (or serve stale while revalidating) Origin

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 MISS and 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.

  1. 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.

  2. 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.

  3. 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
  4. Verify the invalidation took effect. Re-request a purged URL and assert a fresh response. On Cloudflare check the CF-Cache-Status header for MISS or EXPIRED on the first post-purge hit; on CloudFront check X-Cache: Miss from cloudfront; on Fastly check X-Cache: MISS.

    curl -sSI "https://www.example.com/app.css" | grep -i 'cf-cache-status\|x-cache\|age'
  5. 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:

  1. Redeploy the previous-good origin version (or roll back the object in storage).
  2. Confirm origin now serves the good version with a direct origin request that bypasses the CDN.
  3. Purge the affected URLs or tag — narrowly, not everything.
  4. Verify CF-Cache-Status: MISS then a subsequent HIT returning the good content.
  5. If origin cannot be rolled back fast, purge to force a MISS only 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 /page will 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:// vs https:// and /path vs /path/ are distinct cache keys on most CDNs. Purge the canonical form your edge actually stored.
  • Vary explosion: content varying on Accept-Encoding or Accept-Language produces multiple cached variants per URL. A URL purge usually clears all variants, but confirm — and keep your Vary minimal.
  • 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.

Back to CDN Caching & Performance Optimization