---
title: "Cloudflare: Cache Rules"
slug: "cloudflare-cache-rules"
category: "ops"
tags: ["ops", "cloudflare", "cache", "cdn", "performance"]
status: "stable"
last_updated: 2026-05-14
summary: "Cache Rules replace Page Rules: cache-key customization, tiered caching, bypass logic, the cf-cache-status header, and purging strategy."
related: ["[[ops/cloudflare]]", "[[ops/cloudflare-workers]]", "[[ops/cloudflare-security-headers]]", "[[ops/cloudflare-dns]]", "[[ops/github-pages]]", "[[seo/page-speed]]"]
---

## 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 [[ops/github-pages]] or [[ops/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.

```bash
# 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 [[ops/github-pages]] or [[ops/cloudflare-workers]] release job already knows which files changed.

## Related

- [[ops/cloudflare]]
- [[ops/cloudflare-workers]]
- [[ops/cloudflare-security-headers]]
- [[ops/cloudflare-dns]]
- [[ops/github-pages]]
- [[seo/page-speed]]
