Purging Cloudflare Cache by URL vs Cache-Tag

You have just deployed a new build and need stale HTML to disappear from every Cloudflare edge PoP within seconds. The decision between URL-based purge and Cache-Tag purge determines how many API calls you make, how precisely you target content, and whether your plan supports it at all.

Diagnosis: What Makes the Two Methods Different?

URL purge and Cache-Tag purge both call the same Cloudflare REST endpoint (POST /zones/{zone_id}/purge_cache) but with different payload keys. The underlying mechanism diverges at the Cloudflare data layer:

  • URL purge looks up cache objects by their exact request URL. Cloudflare indexes cached objects by URL natively on every plan.
  • Cache-Tag purge looks up cache objects by an arbitrary tag string you attached via the Cache-Tag response header. Cloudflare builds and maintains an inverted index from tag → set of cached object keys. This index is maintained only on Enterprise plans.

Both methods propagate to all edge PoPs within seconds. Neither requires you to know which edge PoP holds the object—Cloudflare broadcasts the purge instruction globally.

Comparison Table

Dimension URL Purge Cache-Tag Purge
Cloudflare plans Free, Pro, Business, Enterprise Enterprise only
Batch size per API call Up to 30 URLs Up to 30 tags (each tag covers unlimited URLs)
Targeting granularity One exact URL per slot All URLs sharing a tag (can be thousands)
Requires response header No Yes — Cache-Tag: your-tag on origin response
Works across hostnames No — URL includes hostname No — tags are zone-scoped
Works for non-deterministic URL sets No Yes
Implementation overhead None — URL is known at build time Moderate — must emit header, maintain tag taxonomy
Typical use for hashed assets Purge HTML entry-point URLs Tag entire HTML route groups by deploy ref
Typical use for un-hashed assets Targeted single-resource purge Purge a content category (e.g., all /blog/ pages)

When URL Purge Fits Fingerprinted Assets

For a frontend application that uses content hashing to fingerprint every JS, CSS, and image file, the set of URLs that need purging after a deploy is small and deterministic: the HTML entry points. A typical Vite SPA has one HTML route (/). A Next.js app with static export might have ten to fifty.

URL purge is the right tool when:

  • Your HTML route count is under 100
  • Routes are known at build time (listed in a sitemap or generated by your static site builder)
  • You are on any Cloudflare plan (Free through Enterprise)
  • You want zero infrastructure overhead—no tag emission, no tag taxonomy
# Purge three HTML entry points after deploy — complete and runnable
ZONE_ID="your_zone_id_here"
API_TOKEN="your_api_token_here"

curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "files": [
      "https://www.example.com/",
      "https://www.example.com/blog/",
      "https://www.example.com/pricing/"
    ]
  }'

The response includes {"success": true} and a result.id for the purge job. The eviction propagates to all edge PoPs within a few seconds.

For sites with more than 30 HTML routes, batch the calls. Read the URL list from a file and POST in chunks of 30:

ZONE_ID="your_zone_id_here"
API_TOKEN="your_api_token_here"
URL_FILE="html_routes.txt"  # one URL per line

# Split into chunks of 30 and purge each chunk
split -l 30 "${URL_FILE}" /tmp/purge_chunk_

for chunk in /tmp/purge_chunk_*; do
  PAYLOAD=$(jq -Rsc 'split("\n") | map(select(length > 0))' < "${chunk}")
  curl -s -X POST \
    "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
    -H "Authorization: Bearer ${API_TOKEN}" \
    -H "Content-Type: application/json" \
    --data "{\"files\": ${PAYLOAD}}"
  sleep 1
done

rm -f /tmp/purge_chunk_*

When Cache-Tag Purge Fits Fingerprinted Assets

Cache-Tag purge becomes necessary when your URL set is large, dynamic, or not enumerable at deploy time. Common scenarios:

  • A CMS-driven site where pages are created by editors and the full URL list is not known at build time
  • A multi-tenant SaaS with hundreds of tenant-specific routes per deploy
  • A large e-commerce site with thousands of category and product pages that share a common layout file

On Enterprise plans, attach Cache-Tag response headers to every HTML response, then purge by tag after each deploy. A single tag can cover the entire site’s HTML.

Emitting Cache-Tag from Nginx

location ~* "\.(html)$" {
    add_header Cache-Tag "html-pages";
    add_header Cache-Control "no-cache";
    try_files $uri =404;
}

location / {
    # For directory-style HTML routes (index.html served without extension)
    add_header Cache-Tag "html-pages";
    add_header Cache-Control "no-cache";
    try_files $uri/index.html $uri =404;
}

Emitting Cache-Tag via Cloudflare Transform Rules

If you cannot change your origin response headers, use a Cloudflare HTTP Response Header Transform Rule:

ZONE_ID="your_zone_id_here"
API_TOKEN="your_api_token_here"

curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_response_headers_transform/entrypoint/rules" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "description": "Attach Cache-Tag to HTML responses",
    "expression": "(http.response.content_type.media_type eq \"text/html\")",
    "action": "rewrite",
    "action_parameters": {
      "headers": {
        "Cache-Tag": {
          "operation": "set",
          "value": "html-pages"
        }
      }
    }
  }'

Purging by Tag

ZONE_ID="your_zone_id_here"
API_TOKEN="your_api_token_here"

curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "tags": ["html-pages"]
  }'

One API call evicts every HTML object in the zone, regardless of how many URLs exist. The CI/CD deploy step becomes a single curl command with no URL list to maintain.

For finer-grained tag strategies, use multiple tags: tag by deploy commit (deploy-abc1234), by content section (section-blog), or by template (template-product-page). You can attach multiple tags to a single response by separating them with commas:

add_header Cache-Tag "html-pages,section-blog,deploy-abc1234";

Then purge by any combination of tags independently.

URL purge vs Cache-Tag purge decision flow A decision tree showing how to choose between URL-based purge and Cache-Tag purge based on plan, HTML route count, and whether URLs are known at build time. Deploy complete Need to purge HTML cache? Enterprise plan? Cache-Tag purge available? No Yes HTML routes ≤ 30? Known at build time? URL purge single API call Yes URL purge batched (30/call) No URLs enumerable? or prefer simplicity? URL purge simpler on Enterprise too Yes No Cache -Tag purge
Decision flow for choosing URL purge vs Cache-Tag purge based on plan and route characteristics.

Verification

After running a purge, confirm the target URLs show cf-cache-status: MISS or EXPIRED on the next request, then HIT on the subsequent one:

ZONE_ID="your_zone_id_here"
API_TOKEN="your_api_token_here"

# 1. Trigger the purge
curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"files": ["https://www.example.com/"]}'

# 2. First request after purge — should be MISS (edge refetching from origin)
curl -sI "https://www.example.com/" | grep -i "cf-cache-status"
# Expected: cf-cache-status: MISS

# 3. Second request — should be HIT (edge has cached the fresh response)
curl -sI "https://www.example.com/" | grep -i "cf-cache-status"
# Expected: cf-cache-status: HIT

For Cache-Tag purge verification, check a sample of URLs that carried the purged tag. All should return MISS on the first request post-purge. If some return HIT, the tag was not emitted on those responses—inspect the response headers from origin to confirm the Cache-Tag header is present.

# Confirm Cache-Tag header is reaching Cloudflare from origin
curl -sI "https://www.example.com/" | grep -i "cache-tag"
# Expected: cache-tag: html-pages

Note: Cloudflare strips the Cache-Tag header before forwarding to browsers, so your end users never see it—only the Cloudflare caching layer reads it.

When to Reconsider

Switch from URL purge to Cache-Tag purge when your HTML route set grows beyond 100 routes, when routes are generated dynamically by editors or users, or when your CI/CD pipeline cannot enumerate the full URL list at deploy time. The up-front cost of emitting tags pays off as soon as batching URL purges becomes a maintenance burden.

Stay with URL purge if you are on Free, Pro, or Business plans (Cache-Tags require Enterprise), or if your site has fewer than 30 HTML routes and the URL list is stable. URL purge involves zero infrastructure change and is always available.

Neither method is right for hashed asset URLs. If you find yourself purging /assets/app.a1b2c3d4.js frequently, the underlying problem is that your asset filenames are not truly fingerprinted. Fix the content hash configuration in your build tool, not the purge strategy. The Cloudflare Cache Rules and purge guide explains how to configure Cache Rules so hashed assets never need purging.