Updated April 2026

CDN Cache Headers Guide — Cache-Control, Vary, Age, X-Cache Explained

Quick Answer

CDNs cache responses based on Cache-Control: public, max-age=N. Check X-Cache: HIT to confirm the CDN is serving from cache. Use Vary: Accept-Encoding to cache separate versions for gzip and brotli. Use CDN-Cache-Control or Surrogate-Control to set different TTLs for CDN vs browser.

CDNs don't just cache — they interpret a specific set of headers to decide what to cache, for how long, and how to key the cache. Understanding these headers lets you control CDN behaviour precisely.

Audit your live CDN cache headers → EdgeFix

The headers CDNs read

HeaderWho sets itWhat it controls
Cache-ControlYour serverPrimary caching instruction for both browser and CDN
CDN-Cache-ControlYour serverCDN-only override — browsers ignore this
Surrogate-ControlYour serverVarnish/Fastly CDN-only TTL (stripped before browser)
VaryYour serverCache key dimensions — cache separate copies per Vary value
ETagYour serverFingerprint for conditional requests
AgeCDNSeconds since CDN cached the response
X-CacheCDNHIT = served from CDN cache, MISS = fetched from origin

Check if your CDN is caching

curl -sI https://example.com/app.js | grep -i "cache\|age\|x-cache"

# Good output:
# Cache-Control: public, max-age=31536000, immutable
# Age: 3600          ← been in CDN cache for 1 hour
# X-Cache: HIT       ← served from CDN, not origin

# Bad output:
# Cache-Control: no-store
# X-Cache: MISS      ← hitting origin every time

Set different TTLs for CDN vs browser

# CDN-Cache-Control — CDN caches for 1 hour, browser for 5 minutes
Cache-Control: public, max-age=300
CDN-Cache-Control: max-age=3600

# Surrogate-Control (Fastly/Varnish) — same idea
Surrogate-Control: max-age=3600
Cache-Control: max-age=300

The Vary header — cache key dimensions

# Cache separate versions for gzip vs brotli
Vary: Accept-Encoding

# Without Vary: Accept-Encoding, CDN serves gzip to browsers expecting brotli
# Always set Vary: Accept-Encoding on compressed responses

# Bad: varies by every header (effectively disables caching at CDN)
Vary: *
Vary: Cookie     # CDN caches separate copy per cookie — cache pollution

Cloudflare-specific headers

cf-cache-status: HIT      # Served from Cloudflare edge
cf-cache-status: MISS     # Fetched from origin
cf-cache-status: EXPIRED  # Was cached, expired, refetching
cf-cache-status: BYPASS   # Cache bypassed (e.g. Cookie present)
cf-cache-status: DYNAMIC  # Cloudflare won't cache this response type
cf-ray: 7a3b4c5d6e7f8g9h  # Request ID for debugging

Vercel-specific headers

x-vercel-cache: HIT    # Served from Vercel edge network
x-vercel-cache: MISS   # Fetched from your function/origin
x-vercel-id: sfo1::...  # Edge region + request ID

Why responses bypass CDN cache

Audit your cache headers live → EdgeFix