Cloudflare Cache control by header

How Cloudflare-CDN-Cache-Control works

A complete guide to CDN-specific cache control headers and how to test them.

1. What is it?

Cloudflare-CDN-Cache-Control is a response header your origin server sends to give Cloudflare separate caching instructions from what browsers and other intermediaries receive via the standard Cache-Control header.

Its sibling, CDN-Cache-Control, works similarly but is forwarded downstream to other CDNs. Cloudflare-CDN-Cache-Control is consumed only by Cloudflare and is never proxied to the browser.

Key benefit: You can cache assets at the Cloudflare edge for a long time while still sending a short Cache-Control: max-age to browsers — without any Cloudflare dashboard changes.

2. Header hierarchy & precedence

When multiple headers are present, Cloudflare follows this priority order (highest to lowest):

  1. Cloudflare-CDN-Cache-Control — Cloudflare-specific; never forwarded downstream
  2. CDN-Cache-Control — for any CDN; forwarded downstream to other CDNs
  3. Cache-Control — standard header; used by browsers and all intermediaries
Header Cloudflare uses it? Proxied downstream? Best for
Cache-Control Yes (fallback) Yes Browser + general cache behavior
CDN-Cache-Control Yes Yes (other CDNs see it) All CDNs in the chain
Cloudflare-CDN-Cache-Control Yes (highest priority) No — stripped before delivery Cloudflare edge only
Important: When Cloudflare-CDN-Cache-Control is present, Origin Cache Control (OCC) is implicitly turned on. This means Cloudflare strictly follows RFC 7234 — for example, it won't cache responses with an Authorization header unless s-maxage, must-revalidate, or public is also present.

3. Cache-Control directives

Cloudflare-CDN-Cache-Control uses the same directives as Cache-Control. Multiple directives are comma-separated.

Cacheability

Directive Meaning
public Any cache may store the response, even if normally non-cacheable.
private Only browser cache; shared caches (like Cloudflare) must not store it.
no-store No cache of any kind may store the response.

Expiration

Directive Meaning
max-age=N Response is stale after N seconds from when it was served by origin.
s-maxage=N Overrides max-age for shared caches (like Cloudflare). Browsers ignore it.
no-cache Cache must revalidate with origin before serving the response.

Revalidation

Directive Meaning
must-revalidate Once stale, caches must not serve without revalidating. Applies to all caches.
proxy-revalidate Same as must-revalidate but only for shared/proxy caches (not browsers).
stale-while-revalidate=N Serve stale for up to N seconds while revalidating in the background.
stale-if-error=N Serve stale for up to N seconds when origin returns an error.

Other

Directive Meaning
no-transform Disables transformations like gzip, Brotli, and Polish at the edge.
immutable Tells browsers not to revalidate unexpired assets. No effect on Cloudflare edge.

4. Origin Cache Control (OCC) behavior

OCC determines whether Cloudflare strictly follows RFC 7234. Free/Pro/Business customers have it on by default. Enterprise customers can toggle it. When Cloudflare-CDN-Cache-Control is present, OCC is always considered on.

Directive OCC Disabled OCC Enabled
no-cache Will not cache Caches and always revalidates
no-store Will not cache Will not cache
max-age=0 Will not cache Caches and always revalidates
s-maxage=0 Will not cache Caches and always revalidates
must-revalidate Ignored; stale is served Does not serve stale; revalidates for CDN and browser
proxy-revalidate Ignored; stale is served Does not serve stale; revalidates for CDN only
no-transform May gzip, Polish, etc. Does not transform body
s-maxage=N (N>1) Same as max-age max-age + proxy-revalidate semantics
immutable Not proxied downstream Proxied downstream (browser-facing only)

5. Use cases & examples

Different TTLs per cache layer

Cache-Control: max-age=14400
Cloudflare-CDN-Cache-Control: max-age=24400
CDN-Cache-Control: max-age=18000
Cache Layer TTL (seconds)
Browser cache 14,400
Cloudflare edge 24,400
Other CDNs 18,000
Network shared cache / origin 84,000 (via s-maxage)

Different stale-if-error per layer

Cache-Control: stale-if-error=400
Cloudflare-CDN-Cache-Control: stale-if-error=60
CDN-Cache-Control: stale-if-error=200
Cache Layer Stale served on 5xx error (seconds)
Cloudflare edge 60
Other CDN 200
Browser / origin cache 400

Common Cache-Control recipes

# Cache a static asset for 24 hours
Cache-Control: public, max-age=86400

# Never cache sensitive content
Cache-Control: no-store

# Cache at CDN only, not in browsers
Cache-Control: private, max-age=3600
Cloudflare-CDN-Cache-Control: public, max-age=86400

# Cache but always revalidate (CDN and browser)
Cache-Control: public, no-cache, must-revalidate

# Cache with background revalidation (stale-while-revalidate)
Cache-Control: max-age=600, stale-while-revalidate=30

# Different edge vs browser TTL
Cache-Control: public, max-age=7200, s-maxage=3600

6. Interactions with other Cloudflare features

Feature Behavior
Edge Cache TTL rule Overrides the TTL in Cloudflare-CDN-Cache-Control / CDN-Cache-Control for how long the asset stays on the edge.
Browser Cache TTL rule Only modifies Cache-Control sent to browsers. Does not modify Cloudflare-CDN-Cache-Control.
Polish Disabled when no-transform directive is present.
Gzip / Brotli compression Disabled when no-transform is present. Original compression from origin is preserved.
Always Online stale-while-revalidate and stale-if-error are ignored when Always Online is enabled.
Surrogate-Control header When Surrogate-Control is present, Cloudflare ignores all Cache-Control directives entirely.
Authorization header When OCC is on (or Cloudflare-CDN-Cache-Control is set), responses with Authorization are only cached if s-maxage, must-revalidate, or public is also present.

7. How to test

Step 1 — Inspect response headers with curl

curl -I https://www.tpimenta.xyz/cache/cloudflare-cdn-cache-control/

Look for these headers in the response:

Step 2 — Understand CF-Cache-Status values

Value Meaning
HIT Served from Cloudflare cache — no origin request made.
MISS Not in cache; fetched from origin and now cached.
BYPASS Cache bypassed (e.g., no-cache directive or cookie present).
EXPIRED Was cached but TTL expired; re-fetched from origin.
REVALIDATED Stale asset revalidated with origin (304 response).
UPDATING Stale but being revalidated asynchronously (stale-while-revalidate).
DYNAMIC Not cached by Cloudflare (dynamic content).

Step 3 — Purge cache and verify MISS → HIT cycle

# Purge a specific URL via Cloudflare API
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://www.tpimenta.xyz/cache/cloudflare-cdn-cache-control/"]}'

# First request after purge — should be MISS
curl -I https://www.tpimenta.xyz/cache/cloudflare-cdn-cache-control/

# Second request — should be HIT (and Age will be > 0)
curl -I https://www.tpimenta.xyz/cache/cloudflare-cdn-cache-control/

Step 4 — Verify Cloudflare isn't forwarding the header

# The Cloudflare-CDN-Cache-Control header must NOT appear in the response
# because Cloudflare strips it before delivery to the client
curl -I https://www.tpimenta.xyz/cache/cloudflare-cdn-cache-control/ | grep -i "cloudflare-cdn"
# Expected output: (empty — header is stripped)

Step 5 — Use Cloudflare Dashboard

Tip: When testing locally or with staging, add -H "Cache-Control: no-cache" to your curl request to bypass the browser/client cache, ensuring you're testing Cloudflare's edge cache behavior directly.

8. Troubleshooting: Origin sends Cloudflare-CDN-Cache-Control: max-age=86400 but CF-Cache-Status is DYNAMIC

CF-Cache-Status: DYNAMIC means Cloudflare is not attempting to cache at all — the resource is treated as uncacheable before TTL headers even come into play. Cloudflare-CDN-Cache-Control controls how long to cache, but the resource must first pass Cloudflare's cacheability checks.

Key insight: Cloudflare-CDN-Cache-Control sets the TTL — but it does not override Cloudflare's default decision of whether to cache. If the resource fails any cacheability check, it gets DYNAMIC regardless of the TTL header.

Root cause #1 — File extension not in Cloudflare's default cache list (most common)

Cloudflare only caches a fixed set of file extensions by default. It does not cache HTML, JSON, XML, or API responses. If the URL is /api/data, /page.html, or anything returning JSON/HTML — Cloudflare marks it DYNAMIC regardless of headers.

Cached by default: css, js, jpg, png, gif, webp, svg, woff2, pdf, zip, mp4 and other static assets.

NOT cached by default: .html, .json, .xml, API endpoints, and any dynamic content.

Fix: Create a Cache Rule in the Cloudflare dashboard to explicitly mark the resource as eligible for cache.

Dashboard → Caching → Cache Rules
Match: URI path contains /your-path
Action: Cache eligibility → Eligible for cache

Root cause #2 — Origin also sends a Cache-Control that prevents caching

Even though Cloudflare-CDN-Cache-Control takes precedence for TTL, if Cache-Control contains no-store, private, or no-cache, Cloudflare may still refuse to cache depending on OCC settings.

Check both headers at the same time:

curl -I https://www.tpimenta.xyz/cache/cloudflare-cdn-cache-control/ | grep -iE "cache-control|cloudflare-cdn"

If you see a conflicting Cache-Control directive, either fix it at the origin or use a Cache Rule to override it.

Root cause #3 — Set-Cookie header in the origin response

If the origin response includes a Set-Cookie header, Cloudflare will not cache it by default — regardless of any cache TTL headers present.

curl -I https://www.tpimenta.xyz/cache/cloudflare-cdn-cache-control/ | grep -i "set-cookie"
Fix: Use a Cache Rule to ignore the Set-Cookie header, or strip it from the response at the edge using a Worker or Transform Rule.

Root cause #4 — Request method is not GET

Cloudflare only caches GET requests. POST, PUT, PATCH, DELETE and all other methods always return DYNAMIC.

Root cause #5 — Request has a Cookie header and Cache Level is Standard

At Standard cache level (the default), requests that include cookies are bypassed for non-static assets. You can change the Cache Level to Ignore Query String or use a Cache Rule to cache regardless of cookies.

Root cause #6 — A Cache Rule is explicitly set to Bypass

An existing Cache Rule with action Bypass cache overrides everything, including Cloudflare-CDN-Cache-Control. Check your Cache Rules in the dashboard for any bypass rules that may match the request path.

Quick diagnostic — run this curl and check all at once

curl -sv https://www.tpimenta.xyz/cache/cloudflare-cdn-cache-control/ 2>&1 | grep -iE \

  "cache-control|cloudflare-cdn|cf-cache|set-cookie|age:|content-type|< HTTP"

Diagnostic checklist

Check How to verify Fix
File type not cached by default (HTML, JSON, API) Look at the URL path and Content-Type header Add a Cache Rule: Eligible for cache
Cache-Control: no-store / private / no-cache from origin curl -I ... | grep cache-control Fix at origin or override with a Cache Rule
Set-Cookie in origin response curl -I ... | grep set-cookie Strip cookie at edge or use Cache Rule to ignore it
Request method is not GET Check method in your request Only GET requests are cacheable
Request sends Cookie header + Standard cache level Check if client sends cookies Use Cache Rule to cache regardless of cookies
Existing Cache Rule set to Bypass Dashboard → Caching → Cache Rules Remove or reorder the bypass rule