Cloudflare Cache Rules and Cache Purge

Cloudflare’s cache layer sits between your users and your origin on every request. Configuring it correctly means fingerprinted assets are served immutably for months while HTML entry points stay fresh—and targeted purge calls handle the rare cases where cache removal is actually needed.

When to Use Cache Rules vs the Legacy Page Rules Interface

Cloudflare’s Cache Rules (introduced in 2023 as the replacement for Page Rules) give you fine-grained control over edge TTL, browser TTL, Cache-Control respect, and cache eligibility—all without consuming the limited Page Rules quota. If you are still using Page Rules for caching behavior, migrate now: Cache Rules support regex path matching, header conditions, cookie conditions, and a much larger rule limit per zone.

Dimension Cache Rules Page Rules
Matching targets URL, hostname, header, cookie, path URL glob only
Rule limit 125 per zone (free), up to 500+ (Enterprise) 3 (free), 20 (Pro/Business)
Edge TTL control Explicit, per-rule Explicit, per-rule
Browser TTL control Yes Yes
Cache-Control respect Configurable Limited
Regex support Yes No
Recommended Yes Migrate away

Prerequisites

  • A Cloudflare zone with at least a Free plan for basic Cache Rules
  • Cache-Tag purge requires an Enterprise plan
  • Prefix purge requires a Pro plan or higher
  • CF_ZONE_ID and CF_API_TOKEN set in your shell or CI environment; your API token needs the Cache Purge permission

Configuration Reference Table

Setting Type Default Effect
cache_eligibility eligible / bypass Depends on origin headers Forces a response into or out of the Cloudflare cache regardless of origin headers
edge_ttl.mode override_origin / respect_origin / bypass_by_default respect_origin Controls how long Cloudflare holds the asset at the edge
edge_ttl.default integer (seconds) none TTL applied when mode is override_origin
browser_ttl.mode override_origin / respect_origin respect_origin Controls the max-age Cloudflare rewrites in the downstream Cache-Control header
browser_ttl.default integer (seconds) none Browser TTL when mode is override_origin
cache_key.ignore_query_strings_order boolean false Normalises ?a=1&b=2 and ?b=2&a=1 to the same cache key
serve_stale boolean false Serves stale content while Cloudflare revalidates from origin
respect_strong_etags boolean false Passes ETags through to the browser unchanged

Step-by-Step Implementation

1. Create a Cache Rule for Fingerprinted Assets

Navigate to Caching → Cache Rules → Create Rule in the Cloudflare dashboard, or use the API. The rule below matches any path under /assets/ and enforces a one-year edge TTL with browser immutability:

# Create a Cache Rule via the Cloudflare API
curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_request_cache_settings/entrypoint/rules" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "description": "Immutable hashed assets under /assets/",
    "expression": "(http.request.uri.path matches \"^/assets/.*\\\\.[a-f0-9]{8,16}\\\\.(js|css|woff2|png|webp|svg)$\")",
    "action": "set_cache_settings",
    "action_parameters": {
      "cache": true,
      "edge_ttl": {
        "mode": "override_origin",
        "default": 31536000
      },
      "browser_ttl": {
        "mode": "override_origin",
        "default": 31536000
      },
      "cache_key": {
        "ignore_query_strings_order": true
      }
    }
  }'

This rule reads: “if the URL path matches a hashed asset filename, cache it at the edge for one year and tell browsers to do the same.” The regex anchors on an 8-character hex segment (the default hash length used in Webpack, Vite, and Rollup builds—increase to 12–16 digits in large monorepos with thousands of chunks).

2. Create a Cache Rule for HTML Entry Points

HTML files must not be cached indefinitely; they are the single reference that changes with every deploy. Apply a short edge TTL and tell Cloudflare to respect Cache-Control: no-cache from your origin:

curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_request_cache_settings/entrypoint/rules" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "description": "Short-lived HTML entry points",
    "expression": "(http.request.uri.path matches \"(\\\\.html$|/$)\")",
    "action": "set_cache_settings",
    "action_parameters": {
      "cache": true,
      "edge_ttl": {
        "mode": "override_origin",
        "default": 30
      },
      "browser_ttl": {
        "mode": "override_origin",
        "default": 0
      }
    }
  }'

A 30-second edge TTL lets Cloudflare serve HTML from cache during a traffic spike while ensuring stale HTML expires in under a minute after a deploy.

3. Set Cache-Control Headers at the Origin

Cloudflare respects Cache-Control from your origin when edge_ttl.mode is respect_origin. For fingerprinted assets, emit the immutable directive from your origin server or CDN upload step:

# Nginx: set immutable Cache-Control for hashed assets
location ~* "^/assets/.*\.[a-f0-9]{8,16}\.(js|css|woff2|png|webp|svg)$" {
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header Vary "Accept-Encoding";
    try_files $uri =404;
}

location ~* "\.(html)$" {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    try_files $uri =404;
}

The immutable directive (RFC 8246) signals to compliant browsers that the resource will never change during max-age, preventing unnecessary conditional revalidations. Combine this with ETag and immutable Cache-Control directives for defence in depth.

4. Enable Tiered Cache (Argo Smart Routing / Cache Reserve)

Cloudflare’s Tiered Cache routes cache misses to a regional upper-tier PoP before falling back to your origin. This reduces origin load during cold-cache deployments of large asset sets. Enable it per zone:

curl -s -X PATCH \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/argo/tiered_caching" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"value":"on"}'

With Tiered Cache active, a new hashed asset uploaded during a deploy warms the upper-tier PoP on first hit from any lower-tier edge node, so subsequent edge misses resolve in milliseconds rather than hitting your origin.

Cloudflare cache purge flow Diagram showing how a browser request flows through Cloudflare edge PoPs and Tiered Cache to the origin, and how targeted purge calls propagate in reverse to invalidate only HTML entry points while hashed assets remain untouched. Browser user request Edge PoP Cache Rules HIT: hashed assets MISS: HTML → upper tier Tiered Cache upper-tier PoP HIT: hashed assets MISS: HTML → origin Origin server / S3 Purge API call POST /zones/{id}/purge_cache (HTML URLs only) Hashed assets: cached 1 year at every layer — no purge needed HTML entry points: 30 s edge TTL, purge on deploy via API
Hashed assets hit the cache at every Cloudflare layer indefinitely. Only the HTML entry-point URL is purged on deploy.

Purge Methods

Cloudflare provides five distinct purge mechanisms. Choosing the right one determines how quickly stale content disappears and how much collateral damage occurs to still-valid cached assets.

Single-File / URL Purge (all plans)

Purge up to 30 URLs per API call. This is the most surgical option: you name each exact URL and only those objects are evicted.

# Purge two specific HTML entry points after a deploy
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/about/"
    ]
  }'

For fingerprinted assets deployed behind content hashing, you almost never need to purge the asset URLs themselves. The filename changes with the content; old filenames become dead references the moment the HTML is updated.

Purge by Prefix (Pro plan and above)

Prefix purge matches all cached URLs whose path begins with a given string. It is faster than enumerating individual URLs when you control a consistent path namespace.

# Purge everything under /static/assets/ (use sparingly — broad scope)
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 '{
    "prefixes": [
      "www.example.com/static/assets/"
    ]
  }'

Do not use prefix purge for fingerprinted assets in normal operation: it evicts every asset under that path from every edge PoP simultaneously, causing a cold-cache stampede against your origin on the next traffic spike.

Purge by Cache-Tag (Enterprise only)

Cache-Tags are arbitrary strings attached to responses via the Cache-Tag response header. Cloudflare reads these tags during caching and allows you to purge all objects sharing a tag with a single API call—regardless of URL.

Set the header on your origin or in a Cloudflare Transform Rule:

# Add Cache-Tag via Cloudflare Transform Rule (API)
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": "Tag HTML pages with deploy ref",
    "expression": "(http.request.uri.path matches \"(\\\\.html$|/$)\")",
    "action": "rewrite",
    "action_parameters": {
      "headers": {
        "Cache-Tag": {
          "operation": "set",
          "value": "html-entry-points"
        }
      }
    }
  }'

Then purge by tag after every deploy:

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-entry-points"]
  }'

A single tag can cover thousands of URLs. This is the right tool when your site has hundreds of HTML routes and enumerating them in URL purge calls is impractical. The comparison between URL-based and tag-based purge is explored in depth in purging Cloudflare cache by URL vs Cache-Tag.

Purge by Hostname

Hostname purge evicts every cached object for a given hostname. It is equivalent to pressing “Purge Everything” scoped to one hostname in a multi-hostname zone.

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 '{
    "hosts": ["www.example.com"]
  }'

Use this only during a full CDN migration or a catastrophic misconfiguration. For routine deploys, hostname purge is always too broad.

Purge Everything

“Purge everything” evicts all cached content in the zone in a single call. The Cloudflare dashboard button does this. Avoid it for asset deployments: it causes every asset—including those that have not changed—to refetch from origin simultaneously.

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 '{"purge_everything": true}'

Reserve purge-everything for configuration emergencies: a wildly wrong Cache-Control header that has been distributed to all edge PoPs, or a zone-wide cache poisoning incident.

Migrating from Page Rules to Cache Rules

If your zone has existing Page Rules for caching, migrate them before adding new Cache Rules. Page Rules and Cache Rules can coexist but running both creates ambiguity about which setting wins. Cache Rules always take precedence over Page Rules for cache behavior.

A typical Page Rule that sets edge TTL looks like this in the legacy format:

URL pattern:  https://www.example.com/assets/*
Setting:      Cache Level = Cache Everything
              Edge Cache TTL = 1 year

The equivalent Cache Rule expression and action:

# Migration: re-create an existing Page Rule as a Cache Rule
curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_request_cache_settings/entrypoint/rules" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "description": "Migrated: cache /assets/* for 1 year",
    "expression": "(http.request.uri.path wildcard \"/assets/*\")",
    "action": "set_cache_settings",
    "action_parameters": {
      "cache": true,
      "edge_ttl": {
        "mode": "override_origin",
        "default": 31536000
      }
    }
  }'

After creating the Cache Rule, disable or delete the corresponding Page Rule. Verify with curl -sI that the cf-cache-status and cache-control headers behave identically before deleting the old rule.

One advantage of the migration: Cache Rules support regex expressions, so a single rule can match asset filenames that contain an 8-character or 12–16-character hex hash without needing multiple URL glob patterns:

"expression": "(http.request.uri.path matches \"^/assets/.*\\.[a-f0-9]{8,16}\\.(js|css|woff2|png|webp|svg)$\")"

Cache Reserve: Persistent Edge Storage for High-Traffic Assets

Cache Reserve (available on Pro plans and above with Cache Reserve enabled) stores cached assets in Cloudflare’s R2 object storage, ensuring that assets are not evicted from the edge even under low traffic. Without Cache Reserve, Cloudflare’s edge may evict cached objects when storage pressure is high—causing cache misses for infrequently requested assets.

For high-traffic frontend applications with many small JS chunks (common in Vite or Webpack code-splitting builds), Cache Reserve prevents the cache-hit-ratio degradation that occurs on assets requested only a few times per day per edge PoP.

Enable Cache Reserve for the zone:

curl -s -X PUT \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/cache/cache_reserve" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"value": "on"}'

Cache Reserve respects your Cache Rule TTL settings. A hashed asset with a 1-year edge TTL will persist in Cache Reserve for up to 1 year, or until purged. Assets that are purged from Cache Reserve must refetch from origin just like a standard cache miss.

When Cache Reserve is active, the cf-cache-status header will show HIT for objects served from R2 storage. The response latency is slightly higher than objects served from in-memory edge cache (typically 5–15 ms additional), but still far below origin latency.

Serving Immutable Hashed Assets (Making Purge Unnecessary)

The correct architecture makes purge calls rare, not frequent. When every asset filename embeds its content hash—as Webpack’s [contenthash:8], Vite’s [hash:8], or Rollup’s [hash] outputs produce—the CDN cache for that filename is permanently valid. The file does not change; the filename changes when the content does.

The only files that need purging after a deploy are the HTML entry points that reference the new hashed filenames. A typical Next.js or Vite SPA has one to a dozen HTML routes. Purging those URLs takes under a second and evicts nothing that is still current.

The cache-key architecture guide explains why filename-based cache keys outperform query-parameter approaches for this pattern. The CI/CD pipeline integration guide shows how to wire the URL purge call into a GitHub Actions deploy workflow automatically.

Cloudflare Workers Integration for Dynamic Cache Key Control

For applications where the default Cloudflare cache key is insufficient—for example, personalized content that must not be served across users, or A/B test variants that must be isolated—a Cloudflare Worker can rewrite the cache key before Cloudflare looks up or stores the object.

For fingerprinted assets this is rarely needed, but it becomes relevant when assets are served from the same URL with different content depending on a request header (such as Accept-Encoding or a custom variant header):

// cloudflare-worker-cache-key.js
// Strips all query parameters from hashed asset requests before cache lookup
// so that ?debug=1 does not create a separate cache key for the same file
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // Normalize hashed asset URLs: strip query strings entirely
    const isHashedAsset = /\/assets\/.*\.[a-f0-9]{8,16}\.(js|css|woff2|png|webp|svg)$/.test(url.pathname);

    if (isHashedAsset) {
      url.search = '';
      const normalizedRequest = new Request(url.toString(), request);
      return fetch(normalizedRequest, {
        cf: {
          cacheEverything: true,
          cacheTtl: 31536000
        }
      });
    }

    // HTML routes: short TTL, pass through
    if (url.pathname.endsWith('/') || url.pathname.endsWith('.html')) {
      return fetch(request, {
        cf: {
          cacheEverything: true,
          cacheTtl: 30
        }
      });
    }

    return fetch(request);
  }
};

Deploy this Worker on the zone’s * route and it intercepts all requests before Cloudflare’s cache lookup. The cf object options override the edge TTL on a per-request basis, supplementing or replacing Cache Rules for advanced routing logic.

Note that Workers-based cache control and Cache Rules both apply. When both are active, the Worker’s cf.cacheTtl takes precedence for objects fetched through the Worker’s fetch() call.

Verification

After configuring Cache Rules and running a purge, verify that Cloudflare is applying the correct behavior using curl -I and checking cf-cache-status:

# Check a hashed asset — expect HIT after first request
curl -sI "https://www.example.com/assets/app.a1b2c3d4.js" \
  | grep -i "cf-cache-status\|cache-control\|age"

# Expected output (second request):
# cf-cache-status: HIT
# cache-control: public, max-age=31536000, immutable
# age: 142

# Check an HTML entry point — expect short age or MISS/EXPIRED
curl -sI "https://www.example.com/" \
  | grep -i "cf-cache-status\|cache-control\|age"

# Expected output after purge:
# cf-cache-status: MISS
# cache-control: no-cache, no-store, must-revalidate

The cf-cache-status header tells you the exact state at the edge PoP that served the response:

Value Meaning
HIT Served from Cloudflare cache
MISS Not in cache; fetched from origin
EXPIRED Was cached; TTL elapsed; refetching from origin
REVALIDATED ETag or Last-Modified matched; 304 from origin; served stale from cache
UPDATING serve_stale active; serving stale while revalidating
BYPASS Cache Rule set cache_eligibility to bypass
DYNAMIC Cloudflare determined the response is not cacheable (e.g., Set-Cookie present)

Run the verification after every Cache Rule change. A DYNAMIC response on a file you expected to be cached almost always means the origin is emitting a Set-Cookie header, which Cloudflare treats as uncacheable by default. Strip cookies from asset responses or add a Cache Rule to ignore cookies for the assets path.

Edge Cases and Known Issues

Tiered Cache and purge propagation delay. When Tiered Cache is enabled, a single-file URL purge propagates to the upper-tier PoP immediately, but lower-tier edge PoPs fetch from the upper tier on their next miss. During the seconds between the purge and the upper-tier refetch, edge PoPs may still serve the stale object. For HTML with a 30-second edge TTL this is negligible; it becomes relevant only if you set a longer edge TTL on HTML.

Cache-Control: no-store defeats Cloudflare caching. If your origin emits no-store rather than no-cache, Cloudflare will not cache the response at all—the cf-cache-status will show DYNAMIC and your Cache Rule override must explicitly set cache_eligibility to eligible to force caching.

Query string normalization. Cloudflare treats ?v=1 and ?v=2 as separate cache keys by default. If your assets use query-parameter cache busting rather than filename hashing, configure cache_key.query_string.exclude.all in your Cache Rule to deduplicate cache keys—or migrate to filename-based fingerprinting.

Plan-gated features. Cache-Tag purge is Enterprise-only. Prefix purge is Pro and above. On a Free plan, only URL purge (up to 30 URLs) and purge-everything are available. Design your purge strategy around the plan you have.

Large monorepo chunk counts. If your build emits more than several hundred hashed chunks, single-file URL purge of every changed asset exceeds the 30-URL-per-call limit and requires batching. However, the correct response is to confirm you actually need to purge those asset URLs. You don’t—only HTML changes need purging.

Performance Impact

The Cloudflare edge cache removes origin bandwidth and latency from every cached asset response. For a typical frontend app:

  • A 1-year edge TTL on hashed assets means a CDN cache hit ratio above 98% for static file types after the initial warm-up period.
  • HTML with a 30-second TTL absorbs traffic spikes without hammering the origin, while ensuring fresh content reaches users within 30 seconds of a deploy.
  • A URL purge call costs one API request and completes in under 200 ms. Running it at the end of a CI/CD deploy pipeline adds no meaningful deploy latency.
  • Tiered Cache reduces origin requests by 60–80% in multi-region deployments by resolving cache misses at the upper-tier PoP.

Pre-Deploy Checklist

  • /assets/** sets 1-year edge and browser TTL with immutable in Cache-Control
  • Cache-Control: public, max-age=31536000, immutable on hashed asset responses
  • Cache-Control: no-cache on HTML responses
  • CF_ZONE_ID and CF_API_TOKEN are stored in CI/CD secrets (not in source)
  • cf-cache-status on at least one HTML URL and one asset URL

FAQ

Do I need to purge fingerprinted asset files after a deploy?

No. Fingerprinted assets have unique filenames derived from their content. When content changes the filename changes; the old URL becomes a dead reference and the new URL has never been cached, so no purge is needed. Only the HTML entry points—which reference the new filenames—need to be purged or allowed to expire.

What does cf-cache-status: DYNAMIC mean for my asset?

DYNAMIC means Cloudflare determined the response should not be cached. The most common causes are: the origin emits a Set-Cookie header (strip it from asset responses), the origin emits Cache-Control: no-store (override with a Cache Rule setting cache_eligibility to eligible), or the path matched a Cache Rule with cache_eligibility: bypass.

How do I purge more than 30 URLs at once?

Batch the URL purge into multiple API calls of 30 URLs each. In a CI/CD script: read your HTML route list from a file, split into chunks of 30, and POST each chunk separately. For sites with hundreds of HTML routes, switch to Cache-Tag purge (Enterprise) or prefix purge (Pro) to avoid the batching overhead.

Should I use edge TTL override or trust my origin Cache-Control headers?

For fingerprinted assets: trust origin headers (respect_origin mode) if your origin already emits max-age=31536000, immutable. Use override_origin mode to enforce TTLs when you cannot control origin headers—for example, when assets are served directly from S3 with default headers. For HTML, override to keep edge TTL short regardless of what the origin sends.