Cloudflare-CDN-Cache-Control worksA complete guide to CDN-specific cache control headers and how to test them.
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.
Cache-Control: max-age to browsers — without any Cloudflare
dashboard changes.
When multiple headers are present, Cloudflare follows this priority order (highest to lowest):
Cloudflare-CDN-Cache-Control — Cloudflare-specific; never
forwarded downstream
CDN-Cache-Control — for any CDN; forwarded downstream to
other CDNs
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 |
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.
Cloudflare-CDN-Cache-Control uses the same directives as
Cache-Control. Multiple directives are comma-separated.
| 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. |
| 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. |
| 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. |
| 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. |
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) |
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) |
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 |
# 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
| 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.
|
curl -I https://www.tpimenta.xyz/cache/cloudflare-cdn-cache-control/
Look for these headers in the response:
CF-Cache-Status — cache result (see table below)Age — seconds the asset has been cached at Cloudflare
Cache-Control — what your origin returned (browser sees
this)
Cloudflare-CDN-Cache-Control — should
not appear; Cloudflare strips it
| 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). |
# 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/
# 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)
-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.
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.
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.
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.
/your-pathCache-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.
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"
Set-Cookie header, or strip it from the response at the
edge using a Worker or Transform Rule.
GET
Cloudflare only caches GET requests. POST,
PUT, PATCH,
DELETE and all other methods always return
DYNAMIC.
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.
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.
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"
| 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 |