Performance guide

Cloudflare Cache Rules: A Safe Rollout Guide

Make public responses faster without turning a cache policy into a data-isolation incident.

Published
Updated
Reading time
21 min read
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.

Acme Shop cache request flow
  1. Request

    A browser requests a versioned asset under /assets/catalog/.

  2. Narrow match

    The Cache Rule matches only that public asset namespace.

  3. 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.

ConcernQuestion it answersPrimary control
Cache RulesWhich matching requests may be cached, bypassed, or given a different edge policy?A conditional Cloudflare rule
Cache keyWhich requests are the same cached representation?URL, selected query parameters, and only the safe variation dimensions
Origin cache headersIs this response public or private, and how should caches handle freshness and revalidation?The origin response's Cache-Control header
PurgeHow 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.

Four controls govern one cached response

Cache eligibility, representation identity, freshness, and removal are separate decisions that must agree for each route.

Download:PNGSVG

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 typeInitial policy
Fingerprinted static assetsCandidate for long-lived public caching; a new filename provides versioned invalidation.
Public images, stylesheets, scripts, and fontsCandidate after checking query-string and content-variation behavior.
Public pages with stable outputCandidate for a short, explicit TTL and a tested purge path.
Login, account, cart, checkout, admin, and authenticated API routesBypass shared caching unless a separately reviewed design proves safe isolation.
Pages varying by session, entitlement, locale, experiment, inventory, or geolocationDo 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 width and format in 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
Representative terminal output; checksum shortened
$ 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

CheckExpected resultStop and investigate when
Positive: origin-authorized versioned asset twiceFirst MISS or EXPIRED, then a HIT; equal SHA-256 hashesHeaders, body, content type, or hash differ
Negative: matching asset without an origin cache directiveBYPASS or equivalent uncached behavior; no edge object is storedThe asset becomes cached despite the absent origin authorization
Negative: /account/orders with a test sessionNo shared-cache eligibility introduced by this ruleA private route shows unexpected cache behavior
Negative: /assets/catalog/ with an irrelevant query stringBehavior matches the existing cache-key policyIt merges a representation that should differ
Failure: origin returns 500 for the test assetError is not retained beyond the chosen origin/cache policyA 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:

StatusMeaning
HITThe object was found in cache and served without contacting the origin.
MISSThe object was not found in cache and was fetched from the origin.
EXPIREDA cached object had passed its TTL and was refreshed from the origin.
REVALIDATEDThe origin confirmed the cached object was still current via a conditional request; the cached bytes were served.
UPDATINGAn expired object was served from cache while stale-while-revalidate refreshed it from the origin in the background.
STALEAn expired object was served from cache because the origin was unreachable, consistent with stale-if-error.
BYPASSThe response was intentionally not cached, per rule or origin instruction.
DYNAMICCloudflare did not consider the response eligible for caching and always went to the origin.
NONE/UNKNOWNCaching 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

  1. Disable or remove the most recent Cache Rule through the same reviewed configuration path used to deploy it.
  2. Restore the prior origin Cache-Control behavior if it changed with the rollout.
  3. Purge the narrowly affected URLs, prefixes, or tags so objects stored under the unsafe policy are not served until expiry.
  4. Re-run the public-object header and body checks, then test an authenticated or personalized journey that was deliberately excluded.
  5. Record the trigger, affected routes, cache statuses, purge scope, and follow-up action before attempting a broader rollout.

Troubleshooting

SymptomLikely causeSafe diagnosticRecovery
Second request remains MISSRule did not match, object is not cacheable, or requests reached different edge locationsUse Cloudflare Trace and compare Cache-Control, host, and pathCorrect the narrow match or origin policy; do not broaden to all HTML
HIT serves the wrong bytesFilename was reused or cache key merged representationsCompare SHA-256 values and inspect every key variationDisable the rule, targeted-purge the asset path, and publish a new fingerprinted name
Signed or transformed URL fails after the changeQuery parameters were omitted or unexpectedly retainedTest one approved variation at a time on a public objectRestore the prior key behavior; inventory parameters before another change
Origin traffic spikes after a purgePurge scope was too broad or cache warming is absentCompare purge target, origin capacity, and CF-Cache-Status distributionStop broad purges, scale/protect origin, then warm only the approved public objects
Account or cart page appears cacheableRule prefix overlaps a stateful routeTrace the exact URL and inspect the active ruleset orderDisable the recent rule and targeted-purge any overlap before retesting
Old image variant still served after a dashboard purgeSingle-file purge from the dashboard does not clear objects keyed on a custom cache key component, Origin, or certain forwarded headersRe-request each known variant and compare CF-Cache-Status and bytesPurge 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 copyCheck whether the route is Cache Reserve-eligible and compare timestamps around the purgeUse 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 currentstale-while-revalidate or stale-if-error is active, intentionally or notInspect the origin's Cache-Control response and Origin Cache Control settingRemove the stale directive for that route, or confirm it is an accepted tradeoff and document it

Authoritative references

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