Overview

Cloudflare’s edge cache is the cheapest performance win on the platform. Cache Rules replaced the older Page Rules in 2023 and offer per-request matchers, custom cache keys, tiered caching, and explicit bypass logic. Stop writing Page Rules; new work belongs in Cache Rules, Origin Rules, or Transform Rules.

Use Cache Rules; Page Rules are deprecated

Page Rules still execute on existing zones, but Cloudflare stopped accepting new feature requests and is sunsetting the system. Cache Rules are the replacement.

  • Per-zone Cache Rules support up to 50 rules on the free plan and far more on paid.
  • The matcher uses the same expression engine as WAF and Transform Rules: http.request.uri.path eq "/api/health".
  • Order matters; rules evaluate top to bottom, and the first match wins for cache eligibility.
  • Drift between Page Rules and Cache Rules is a common bug. Migrate in one pass and disable the old rules.

For migrations larger than a handful of rules, drive the change through Terraform with the cloudflare_ruleset resource and review the diff before applying.

Customize the cache key to match content boundaries

The cache key is the tuple Cloudflare hashes to look up a cached response. The default key is host plus path plus query string; everything else (headers, cookies) is ignored.

  • Include a header in the key when the response varies by it: Accept-Language, X-Theme, CF-Device-Type.
  • Include specific query params; exclude tracking params (utm_*, gclid, fbclid) to avoid cache fragmentation.
  • Include a cookie only when authentication or A/B routing demands it. A user-specific cookie destroys the cache hit rate; reach for a Worker or a separate path instead.
# Cache Rule (expression)
http.request.uri.path matches "^/products/.*"
# Cache key settings
include query params: id, color
include header: CF-Device-Type
ignore query params: utm_source, utm_medium, utm_campaign, gclid

Set cache eligibility to “Eligible for cache” and pick an Edge TTL that matches how fast the content changes.

Turn on tiered caching for upper-tier hits

Tiered caching routes cache misses through an upper-tier PoP before they hit the origin. The upper tier holds the canonical copy; lower tiers fill from it.

  • Smart Tiered Caching picks the upper tier automatically based on origin location. Turn it on; it is free on paid plans.
  • The benefit grows with origin distance and traffic from many regions. A site with a US-East origin and global traffic sees a 30 to 60 percent reduction in origin requests.
  • Pair tiered caching with a long Edge TTL (max-age 86,400 or longer) so the upper tier holds the asset between regional misses.

For static assets fronted by github-pages or cloudflare-r2, tiered caching plus a long Edge TTL eliminates almost all origin traffic.

Bypass cache for authenticated or dynamic paths

The default cache passes through Cache-Control: no-store, cookies named _session, and anything the origin marks private. Make the bypass explicit anyway.

# Cache Rule: bypass API
http.request.uri.path starts_with "/api/"
=> Cache eligibility: Bypass cache

# Cache Rule: bypass authenticated dashboard
http.request.uri.path starts_with "/app/" and
http.request.cookies["session"] != ""
=> Cache eligibility: Bypass cache

Bypass is safer than guessing at TTLs for dynamic content. The cost is a full trip to origin on every request; pair the bypass with cache headers from the origin (Cache-Control: private, no-store) so downstream caches behave too.

Read cf-cache-status to diagnose cache behavior

Every response from Cloudflare carries a cf-cache-status header. Treat it as the source of truth.

  • HIT: served from the edge cache. The goal.
  • MISS: not cached at the edge; fetched from origin and now cached.
  • EXPIRED: was cached but the TTL elapsed; refetched from origin.
  • REVALIDATED: stale-while-revalidate; served stale while a background refresh runs.
  • DYNAMIC: not eligible for cache (cookies, method, headers).
  • BYPASS: matched a bypass rule.
  • UPDATING: stale-while-error; served stale because origin returned an error.

A site that ships mostly DYNAMIC is missing a Cache Rule. A site with high EXPIRED rate needs a longer Edge TTL. A site with BYPASS on assets has a misconfigured rule.

Purge surgically, not aggressively

Cloudflare offers four purge modes; pick the smallest one that works.

  • Purge by URL: invalidate a single asset. Use this 95 percent of the time, wired into the deploy pipeline.
  • Purge by cache tag (Enterprise): tag responses with Cache-Tag: home,nav from the origin, then purge by tag on content updates.
  • Purge by hostname: clear a single subdomain. Useful for staging cutovers.
  • Purge everything: clears the entire zone. Costs minutes of origin load and a temporary latency spike; avoid as a routine action.
# Purge specific URLs after deploy
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"]}'

Wire targeted purges into the deploy pipeline; the github-pages or cloudflare-workers release job already knows which files changed.