Overview

Cloudflare’s edge cache is the cheapest performance win on the platform, and Cache Rules are how you control it. A rule matches requests with the same expression engine as WAF and Transform Rules (http.request.uri.path starts_with "/api/") and sets cache eligibility, TTLs, and cache-key behavior.

Write Cache Rules, not Page Rules

Page Rules are deprecated. Cache Rules take precedence over Page Rules on the same path, so run one system and migrate in a single pass.

  • Quota per zone: Free 10, Pro 25, Business 50, Enterprise 300 (source).
  • Rules stack. If several matching rules set the same setting, the last matching rule wins. Page Rules were first-match, so a straight port can change behavior.
  • Use Terraform’s cloudflare_ruleset for large sets and review the diff before applying.

Know the defaults before writing rules

Cloudflare does not cache HTML or JSON by default; it caches a fixed list of static extensions (images, JS, CSS, media, archives, robots.txt). It also skips any response with Set-Cookie or Cache-Control of private, no-store, no-cache, or max-age=0. Free, Pro, and Business zones cache files up to 512 MB. To cache HTML, add a rule that sets “Eligible for cache” and an Edge TTL, and keep the bypass rules below.

Customize the cache key only on Enterprise, and plan around it otherwise

The default key is the full URL (scheme, host, path, query string) plus a few request headers. Custom keys that include or exclude specific query parameters, headers, cookies, host, or user features (device type, geo, language) are Enterprise-only (cache keys). On other plans, “ignore query string” and “sort query string” are available; to drop only tracking parameters (utm_*, gclid), strip them in a Worker or at the origin. Never key on a per-user cookie: it destroys the hit rate. Use a separate path or a Worker.

Turn on Smart Tiered Cache

Tiered caching sends misses through an upper-tier data center before the origin, so lower tiers fill from the upper tier. Smart Tiered Cache picks the upper tier from the origin’s location and is available on every plan; Regional and Custom topologies are Enterprise-only. Pair it with a long Edge TTL (a day or more) on fingerprinted assets. It removes most origin traffic for static assets behind github-pages or cloudflare-r2.

Bypass cache for dynamic and authenticated paths

# Cache Rule: bypass API and dashboard
(http.request.uri.path starts_with "/api/") or
(http.request.uri.path starts_with "/app/" and http.cookie contains "session=")
=> Cache eligibility: Bypass cache

Also send Cache-Control: private, no-store from the origin so downstream caches comply. Because the last matching rule wins, put bypass rules after any broad “cache everything” rule.

Read cf-cache-status to diagnose

ValueMeaning
HITServed from Cloudflare’s cache
MISSCacheable, not in cache; fetched from origin
EXPIREDCached copy expired; refetched from origin
STALEExpired copy served because the origin could not be reached
REVALIDATEDOrigin confirmed the copy via If-Modified-Since or If-None-Match
UPDATINGExpired copy served while the origin refreshes it in the background
DYNAMICNot eligible for cache; no cache lookup
BYPASSEligible, but origin headers (no-store, Set-Cookie, size) prevented caching
NONE / UNKNOWNCloudflare generated the response (Worker, WAF block, redirect)

Mostly DYNAMIC on pages you expect cached means no rule makes them eligible. A high EXPIRED rate calls for a longer Edge TTL.

Purge the smallest scope that works

Purge by URL, tag, prefix, or hostname is available on every plan; purge everything is the last resort because it floods the origin with misses. Tag purging needs a Cache-Tag: home,nav response header from the origin. Rate limits differ by plan: on Free, hostname, tag, prefix, and purge-everything calls are limited to 5 per minute, and URL purges to 100 URLs per request (purge docs).

curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"files":["https://example.com/assets/app.css","https://example.com/index.html"]}'

The release job already knows which files changed, so wire the targeted purge into it.