Performance guide
Cloudflare Cache Rules: A Safe Rollout Guide
Make public responses faster without turning a cache policy into a data-isolation incident.
On this page
Overview
Outcome and Acme Shop scenario
A managed edge-orchestration layer lives or dies on exactly this kind of discipline: cache rules, keys, and purges are decided once but applied to every subsequent request across every provider fronting the origin, so a narrow, provable rollout is what keeps Performance gains from becoming a Visibility or Security incident at scale. The Acme Shop scenario below shows that discipline end to end, independent of which CDN sits in front of the origin.
Outcome
Acme Shop will cache fingerprinted public catalogue assets under
https://www.acme-shop.example/assets/catalog/ only when the origin authorizes
caching with its response directives. A reviewer can see the exact match,
verify a cold MISS becomes a HIT, compare the served bytes to the release
artifact, and disable one rule plus purge only the affected path if the result
is wrong.
Running scenario
Acme Shop publishes shoe-card.8e21a3.js with every catalogue release. The
asset is public and its filename changes whenever its bytes change. Its account,
cart, checkout, search, and /api/ routes remain outside this rollout.
- Request
A browser requests a versioned asset under /assets/catalog/.
- Narrow match
The Cache Rule matches only that public asset namespace.
- Serve or fill
An edge HIT serves the object; a first request can fetch and store the public asset.
Stateful and authenticated routes are outside this rollout and do not match the rule.
Cloudflare caching is an application behavior, not just a performance switch. A correct rollout identifies which responses are public, defines the representation each request may receive, proves that the edge is serving the intended object, and provides a narrow way to remove it when content changes.
This guide is for DevOps and web engineers operating a proxied Cloudflare zone. Start with an asset or an explicitly public page. Do not use the first rollout to cache account pages, authenticated APIs, carts, checkout, search results personalized by identity, or any response that can contain one user's data.
Cache Everything is not a safety decision
A rule that makes broad dynamic traffic eligible for caching can cache private or personalized responses if its match and cache key do not preserve isolation. Exclude authenticated and stateful routes first; only make narrowly defined, demonstrably public responses eligible.
Prerequisites
- A hostname whose DNS record is proxied through Cloudflare. Cache Rules apply to proxied traffic.
- Access to the Cloudflare zone and to the origin configuration, plus a change record with a named owner and rollback time.
- A staging hostname or a small, public production path that has no login, cookie-based variation, authorization, or user-specific output.
- A representative test URL and a known expected response body, status, content type, and cache headers.
- A deployment or content-publishing workflow that can purge the affected objects after a release.
Record a baseline before changing anything. The following command is safe for a public test object; inspect the response headers and save the result with the change record:
curl -sS -I https://www.example.com/assets/app.4d2c1.js
Look for the origin's Cache-Control intent and Cloudflare's CF-Cache-Status. A MISS on the first request is normal for a cold object. Run the same request again after the object has had an opportunity to populate the cache and confirm the status, response headers, and body are still appropriate. Do not treat one header from one edge location as proof for every route or region.
Keep four concerns separate
These controls solve different problems. Treating them as interchangeable is a common source of stale content and data exposure.
| Concern | Question it answers | Primary control |
|---|---|---|
| Cache Rules | Which matching requests may be cached, bypassed, or given a different edge policy? | A conditional Cloudflare rule |
| Cache key | Which requests are the same cached representation? | URL, selected query parameters, and only the safe variation dimensions |
| Origin cache headers | Is this response public or private, and how should caches handle freshness and revalidation? | The origin response's Cache-Control header |
| Purge | How is an already stored representation removed after a change? | A targeted invalidation workflow |
Cloudflare documents Cache Rules as controls for cache eligibility, duration, and related cache settings. A cache key is the identifier used to store and retrieve a cached file; it is not an authorization boundary. Cloudflare's default key includes the full URL, including the query string, alongside other request properties. Changing it can consolidate distinct requests into one object, or split one object into many. Both outcomes must be intentional.
Cache eligibility, representation identity, freshness, and removal are separate decisions that must agree for each route.
Origin headers remain the clearest expression of application intent. For example, Cache-Control: public, max-age=86400 communicates a public, cacheable response, while private tells shared caches not to store a response and no-store tells caches not to store it at all. Cloudflare can respect, augment, or override origin behavior depending on Cache Rule settings and Origin Cache Control; verify the resulting behavior rather than assuming the header alone wins. Origin Cache Control is documented as enabled by default for Free, Pro, and Business zones, and those plans cannot turn it off; only Enterprise can substitute Cloudflare's own defaults for the origin's directives, so on every other plan the origin header is not merely "the clearest" signal, it is close to the only one.
Stage 1: classify routes before creating a rule
Build a small route table with the application owner. Include at least these categories:
| Route type | Initial policy |
|---|---|
| Fingerprinted static assets | Candidate for long-lived public caching; a new filename provides versioned invalidation. |
| Public images, stylesheets, scripts, and fonts | Candidate after checking query-string and content-variation behavior. |
| Public pages with stable output | Candidate for a short, explicit TTL and a tested purge path. |
| Login, account, cart, checkout, admin, and authenticated API routes | Bypass shared caching unless a separately reviewed design proves safe isolation. |
| Pages varying by session, entitlement, locale, experiment, inventory, or geolocation | Do not share-cache until the exact variation and invalidation design is reviewed. |
Check how the origin varies output. Vary is not automatically a complete Cloudflare cache-key design: Cloudflare documents specific support for configured Vary settings and particular cases. If a response changes because of a query parameter, header, cookie, or client characteristic, either include only the necessary safe dimension in the key or keep that response out of the shared cache. Never add an authorization token, session identifier, or other secret to a cache key as a shortcut; doing so creates a sensitive identifier in cache configuration and is rarely the right application design.
Cache key configuration: defaults, custom keys, and plan limits
The default Cloudflare cache key is not "the URL." It is built from the scheme, the resolved host, the URI including its query string, the Origin request header, and a small set of other request properties Cloudflare evaluates internally. Two requests that differ in any of those inputs are, by default, two different stored objects. This matters before you touch a single setting: if Acme Shop's catalogue assets are ever served over both HTTP and HTTPS at the edge, or through two different hostnames pointing at the same origin, the default key already treats them as separate cache entries with separate MISS/HIT histories.
Cache key configuration on Free, Pro, and Business plans is limited to a few toggles: cache by device type (mobile, desktop, tablet), ignore query string entirely, sort query string parameters into a canonical order, and cache deception armor (a protection against origin responses being cached under an attacker-controlled path). Full custom cache keys — selectively including or excluding specific query-string parameters with their values, keying on request headers or cookies, choosing the original versus Cloudflare-resolved host, or keying on device type, country, or first Accept-Language code — require an Enterprise plan. Confirm which tier you are on before designing a rollout around a capability that may not be exposed in your zone.
Acme Shop's catalogue also serves resized product images through the same /assets/catalog/ prefix, with width and format query parameters that legitimately produce different bytes for the same path. Two designs are both defensible:
- Ignore query string is wrong here. It would collapse every width and format variant onto one stored object, so whichever variant is requested first becomes what every client receives until the object expires or is purged.
- Include only
widthandformatin the key (Enterprise), or leave the default key in place and enumerate the small number of variants your image pipeline actually emits, so the cache-key change matches the application's real variation surface rather than "every parameter" or "no parameters."
Whichever you choose, the Custom Cache Key example and the Cache keys reference are the source of current field names; do not guess at a parameter name from an older screenshot. Note too that the Prefetch feature is documented as incompatible with custom cache keys, so if Acme Shop's storefront relies on <link rel="prefetch"> hints for catalogue navigation, verify that behavior explicitly rather than assuming it composes with a custom key.
Stage 2: make the origin intent explicit
Prefer to make an asset or page's safety visible at the origin before adding an edge override. For a public immutable asset, use a response policy appropriate to the application's release model, such as:
Cache-Control: public, max-age=31536000, immutable
Only use a long lifetime when the URL changes whenever the bytes change. For a public HTML page that changes independently of its URL, choose a shorter lifetime and confirm that the publishing workflow can purge it. For sensitive pages, use a policy such as Cache-Control: no-store when the application requires that neither browser nor intermediary store the response; private is appropriate when browser caching is acceptable but shared caching is not.
s-maxage can set a shared-cache lifetime distinct from browser max-age. Cloudflare notes that it has revalidation implications, so do not combine directives by pattern matching. Test the exact combination your origin returns. The Cloudflare Origin Cache Control reference describes how no-cache, no-store, private, s-maxage, and stale directives interact with its cache behavior.
Two stale directives are worth designing for deliberately rather than discovering by accident. stale-while-revalidate lets Cloudflare serve an expired object while it revalidates with the origin in the background; a matching request during that window shows CF-Cache-Status: UPDATING, but Cloudflare's documentation notes this is suppressed when s-maxage is present, because s-maxage implies proxy-revalidate. stale-if-error lets Cloudflare keep serving the last good object when the origin starts returning server errors, which is a reasonable resilience posture for a public catalogue asset but a dangerous default for anything that must reflect current state, such as inventory or pricing. Decide per route whether serving something-stale is better than serving an error, and say so explicitly in the change record rather than relying on whatever the origin happened to send.
Stage 3: configure Cloudflare Cache Rules for one narrow match
This is where Cloudflare Cache Rules stop being a concept and become a reviewable artifact. Create a rule that matches only the public test object or an equivalent, immutable asset namespace. Keep the first match simple: one hostname and one path prefix, with an explicit exclusion for any stateful or authenticated path that could overlap.
Make one decision per rollout:
- Set cache eligibility only after the response is known to be public.
- Set an edge TTL only when it is deliberately different from origin intent.
- Change the cache key only when you can explain every representation that will be merged or split.
Avoid a broad "cache all HTML" policy. It commonly captures login-adjacent pages, application shells with user state, error responses, or routes that differ by cookie and authorization. If a future use case truly needs dynamic caching, design it with the application team: document the identity model, all variation inputs, origin headers, TTL, invalidation, and a test that proves one user cannot receive another user's response.
Cloudflare allows rules to be created through its dashboard, API, or Terraform. Use the delivery mechanism your team can review, version, and roll back. The authoritative Cache Rules documentation and rule examples are the source of current fields and capabilities.
Acme Shop rule under review
The following Terraform declares the http_request_cache_settings ruleset
phase documented by Cloudflare. It makes one immutable, public path eligible
but uses bypass_by_default: Cloudflare caches it only when the origin supplies
an applicable cache directive, and bypasses the edge cache when it does not.
Review terraform plan before applying it; do not paste a zone ID or API token
into the file.
resource "cloudflare_ruleset" "acme_catalog_assets" {
zone_id = var.zone_id
name = "Acme Shop public catalogue assets"
kind = "zone"
phase = "http_request_cache_settings"
rules = [{
ref = "cache_public_catalog_assets"
description = "Cache only versioned public catalogue assets"
expression = "(http.host eq \"www.acme-shop.example\" and starts_with(http.request.uri.path, \"/assets/catalog/\"))"
action = "set_cache_settings"
action_parameters = {
cache = true
edge_ttl = { mode = "bypass_by_default" }
browser_ttl = { mode = "respect_origin" }
}
}]
}
The equivalent API rule uses action: "set_cache_settings" in the
http_request_cache_settings phase. Cloudflare's API example warns that a
ruleset PUT replaces its full rule list, so retrieve and review the existing
ruleset rather than sending a one-rule replacement request blindly.
The same action surface also carries settings this guide's first rollout does not need but you will meet on the next one: a per-status-code TTL override (cache a 404 for a few seconds without caching a 200 the same way), "serve stale while revalidating" and "respect strong ETags" toggles, an origin error page pass-through option, and Cache Reserve eligibility with a minimum file size. Treat each as its own reviewed decision with its own test, the same way you treated edge_ttl and browser_ttl above; a second setting bundled into "the caching rule" without its own justification is how a narrow rollout quietly grows into a broad one.
Stage 4: verify cache behavior and content correctness
Warm and inspect the single public URL twice:
curl -sS -I https://www.example.com/assets/app.4d2c1.js
curl -sS -I https://www.example.com/assets/app.4d2c1.js
Then verify the object itself, not just its headers. Compare its checksum to the release artifact where possible:
curl -sS https://www.example.com/assets/app.4d2c1.js | shasum -a 256
For Acme Shop, run the safe public-object checks from a controlled client. When
the origin returns the documented public cache directive, the first result can
be a MISS and the second should normally be a HIT from the same edge after
the object is stored. Without that directive, bypass_by_default should leave
the object uncached. Cache status is evidence for this request, not a global
guarantee.
asset='https://www.acme-shop.example/assets/catalog/shoe-card.8e21a3.js'
curl -sS -D /tmp/acme-headers-1 -o /tmp/acme-asset-1 "$asset"
curl -sS -D /tmp/acme-headers-2 -o /tmp/acme-asset-2 "$asset"
shasum -a 256 /tmp/acme-asset-2 dist/shoe-card.8e21a3.js
$ grep -i '^cf-cache-status:' /tmp/acme-headers-1
cf-cache-status: MISS
$ grep -i '^cf-cache-status:' /tmp/acme-headers-2
cf-cache-status: HIT
$ shasum -a 256 /tmp/acme-asset-2 dist/shoe-card.8e21a3.js
7b1d... /tmp/acme-asset-2
7b1d... dist/shoe-card.8e21a3.js
Validation matrix
| Check | Expected result | Stop and investigate when |
|---|---|---|
| Positive: origin-authorized versioned asset twice | First MISS or EXPIRED, then a HIT; equal SHA-256 hashes | Headers, body, content type, or hash differ |
| Negative: matching asset without an origin cache directive | BYPASS or equivalent uncached behavior; no edge object is stored | The asset becomes cached despite the absent origin authorization |
Negative: /account/orders with a test session | No shared-cache eligibility introduced by this rule | A private route shows unexpected cache behavior |
Negative: /assets/catalog/ with an irrelevant query string | Behavior matches the existing cache-key policy | It merges a representation that should differ |
Failure: origin returns 500 for the test asset | Error is not retained beyond the chosen origin/cache policy | A success object is replaced with an unexpected error or origin load spikes |
Test every intended variation. For example, if a public image endpoint legitimately uses width and format query parameters, request each approved combination and confirm each response has the expected dimensions and type. Also request an irrelevant tracking parameter and confirm it behaves according to the chosen key policy. Do this only against non-sensitive public objects.
CF-Cache-Status values worth knowing
The validation matrix above only distinguishes MISS and HIT, but Cloudflare's edge reports several other statuses, and a diagnosis is faster when you recognize them on sight instead of treating anything unfamiliar as a bug:
| Status | Meaning |
|---|---|
HIT | The object was found in cache and served without contacting the origin. |
MISS | The object was not found in cache and was fetched from the origin. |
EXPIRED | A cached object had passed its TTL and was refreshed from the origin. |
REVALIDATED | The origin confirmed the cached object was still current via a conditional request; the cached bytes were served. |
UPDATING | An expired object was served from cache while stale-while-revalidate refreshed it from the origin in the background. |
STALE | An expired object was served from cache because the origin was unreachable, consistent with stale-if-error. |
BYPASS | The response was intentionally not cached, per rule or origin instruction. |
DYNAMIC | Cloudflare did not consider the response eligible for caching and always went to the origin. |
NONE/UNKNOWN | Caching was never evaluated for this response, for example a Worker-generated response outside the cache path. |
UPDATING and STALE in particular are correct behavior, not failures, when Stage 2's stale directives are intentional; the failure mode is seeing them on a route where you did not expect the origin to ever be treated as slow or unreliable.
Use Cloudflare Trace to determine whether a Cache Rule and cache-key setting applied to a specific URL. Pair that result with origin request logs and application telemetry: a cache hit should reduce origin work without changing status, content type, security headers, or the response body. Measure cache hit ratio and origin load over a representative period, not just a successful single request.
Watch cache behavior as one signal, across every edge you run
Hit ratio, CF-Cache-Status distribution, and origin load are exactly the
kind of signal a managed observability layer such as MYO is built to track
continuously and correlate across providers, so a cache-key regression on
one edge surfaces as a trend before it becomes a paged incident, rather than
after. That discipline holds whether Cloudflare is the only edge in front of
the origin or one of several behind a shared orchestration layer.
Stage 5: run a targeted edge cache purge before republishing
Purging removes cached content; it does not decide whether a future request is eligible for caching. Keep it separate from rule design.
For a changed object, prefer the narrowest invalidation that matches the release: Cloudflare recommends single-file purge by URL, and also supports hostname, prefix, tag, cache-key-resource, and full-zone methods. Tag-, prefix-, and hostname-based purge were Enterprise-only for years; Cloudflare's 2025 "Instant Purge" rollout made every purge method available on every plan, so do not assume a smaller Acme Shop-sized account is limited to single-file and full-zone purge — check the current purge cache reference for the method and rate limit that actually apply to your plan rather than an older assumption. A full purge is still a last resort on any plan, because it turns normal traffic into origin misses and can create avoidable load.
An edge cache purge issued from the dashboard has a specific, easy-to-miss gap: single-file purge performed through the dashboard does not clear objects whose cache key incorporated a custom cache key component, the Origin header, or certain forwarded headers (X-Forwarded-Host, X-Host, X-Forwarded-Scheme, X-Original-URL, X-Rewrite-URL, Forwarded). If Acme Shop later adopts a custom cache key for the resized-image variants described earlier, a dashboard purge of the base URL can silently leave old variants in cache; use the API or a purge method that matches the key design instead, and confirm with a fresh request rather than trusting the purge response alone.
If catalogue assets ever become eligible for Cache Reserve (the persistent, longer-retention tier layered on top of the normal edge cache), remember that "Purge Everything" performs a soft purge against Cache Reserve: it forces revalidation on the next request but does not delete the stored object, so stale bytes can still be billed and briefly reachable during a race. A single-file purge, by contrast, removes both the edge and the Cache Reserve copy together. Treat Cache Reserve as a second tier with its own purge behavior, not an extension of the edge cache you already reasoned about.
After a targeted purge, request the URL again and verify that the new representation reaches the edge, then repeat the header and checksum checks. If the cache key varies on a property relevant to the object, ensure the purge method covers the stored variants; Cloudflare documents the additional header, query-string, host, and prefix information required for some purge types.
Do not place a broadly privileged API token in a shell history, ticket, or CI log. Give the publishing system a least-privilege token, keep it in the secret store, and monitor failed or rate-limited purge requests. See Cloudflare's purge cache reference and purge by single-file guide for current API requirements and limits.
Rollback
- Disable or remove the most recent Cache Rule through the same reviewed configuration path used to deploy it.
- Restore the prior origin
Cache-Controlbehavior if it changed with the rollout. - Purge the narrowly affected URLs, prefixes, or tags so objects stored under the unsafe policy are not served until expiry.
- Re-run the public-object header and body checks, then test an authenticated or personalized journey that was deliberately excluded.
- Record the trigger, affected routes, cache statuses, purge scope, and follow-up action before attempting a broader rollout.
Troubleshooting
| Symptom | Likely cause | Safe diagnostic | Recovery |
|---|---|---|---|
Second request remains MISS | Rule did not match, object is not cacheable, or requests reached different edge locations | Use Cloudflare Trace and compare Cache-Control, host, and path | Correct the narrow match or origin policy; do not broaden to all HTML |
HIT serves the wrong bytes | Filename was reused or cache key merged representations | Compare SHA-256 values and inspect every key variation | Disable the rule, targeted-purge the asset path, and publish a new fingerprinted name |
| Signed or transformed URL fails after the change | Query parameters were omitted or unexpectedly retained | Test one approved variation at a time on a public object | Restore the prior key behavior; inventory parameters before another change |
| Origin traffic spikes after a purge | Purge scope was too broad or cache warming is absent | Compare purge target, origin capacity, and CF-Cache-Status distribution | Stop broad purges, scale/protect origin, then warm only the approved public objects |
| Account or cart page appears cacheable | Rule prefix overlaps a stateful route | Trace the exact URL and inspect the active ruleset order | Disable the recent rule and targeted-purge any overlap before retesting |
| Old image variant still served after a dashboard purge | Single-file purge from the dashboard does not clear objects keyed on a custom cache key component, Origin, or certain forwarded headers | Re-request each known variant and compare CF-Cache-Status and bytes | Purge via the API with the matching key parameters, or purge by prefix/tag instead |
| Stale bytes reappear briefly after "Purge Everything" | Purge Everything is a soft purge against Cache Reserve; it revalidates but does not delete the stored copy | Check whether the route is Cache Reserve-eligible and compare timestamps around the purge | Use single-file purge for Cache Reserve-eligible objects, or budget for the revalidation window |
CF-Cache-Status shows UPDATING or STALE on a route that should always be current | stale-while-revalidate or stale-if-error is active, intentionally or not | Inspect the origin's Cache-Control response and Origin Cache Control setting | Remove the stale directive for that route, or confirm it is an accepted tradeoff and document it |
Related guides
Authoritative references
- Cloudflare Cache Rules
- Cloudflare Cache Rules settings reference
- Cloudflare Cache keys
- Cloudflare Origin Cache Control
- Cloudflare Cache Reserve
- Cloudflare Purge cache
- Cloudflare Trace a request
- IETF RFC 9111: HTTP Caching
Make Cloudflare Cache Rules safe to operate at scale
Talk to Optimi about cache-key design, edge cache purge workflows, and cross-provider observability that keep Performance gains from turning into Security or Visibility gaps.
Discuss cache architecture