CDN Debugging Beginner 📅 September 11, 2026 ⏱ 11 min read 👤 Alex Petrov

CDN Header Inspection and Debugging: Reading Cache Status Headers

AP
Alex Petrov — DevOps & Infrastructure Engineer
10+ years operating edge networks, Nginx/Varnish, and multi-CDN failover for high-traffic platforms. Focused on cache efficiency and observability.

What do CDN cache status headers mean? They are HTTP response headers like X-Cache, CF-Cache-Status, and Age that tell you whether a request was a HIT, MISS, BYPASS, or REVALIDATED at the edge, so you can instantly verify caching and debug performance issues.

What Are CDN Cache Status Headers?

CDN cache status headers are standard and vendor-specific HTTP response headers that expose the edge server’s caching decision for a single request.

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.99% (multi-PoP)99.9% (single DC)10x fewer outages

Every time your browser or curl hits a CDN-enabled URL, the edge PoP must decide to serve from RAM/disk, fetch from origin, or skip the cache entirely. That decision is serialized into headers before the response leaves the edge.

The most universal header is X-Cache with values like HIT or MISS, but each provider adds its own nuance. Cloudflare uses CF-Cache-Status, Fastly uses X-Cache and X-Cache-Hits, Akamai uses X-Cache plus X-Akamai-Request-ID, and AWS CloudFront uses X-Cache with Hit from cloudfront.

These headers are not part of the cached body itself; they are hop-by-hop metadata added at the edge. They are safe to expose to clients and are invaluable for performance auditing. If you understand how a CDN works at the request-flow level, these headers become your real-time window into that flow.

Why These Headers Matter for Debugging

Without cache status headers, you are guessing whether your performance problem is at the edge, the origin, or the browser.

A page that feels slow might have a 98% HIT ratio globally but a 20% HIT ratio for logged-in users because cookies bust the cache. The header tells you in one glance if that specific request was cached.

Headers also validate your Cache-Control strategy. If you set max-age=86400 but still see MISS on every reload, something upstream is sending no-store, private, or Vary: *.

For SEO and Core Web Vitals, consistent HITs directly lower Time to First Byte (TTFB) and Largest Contentful Paint (LCP). Learning to read these headers is the fastest way to confirm that your CDN improvements for Core Web Vitals are actually taking effect in the wild, not just in synthetic lab tests.

Finally, headers give you a shared language with support teams. Instead of saying “the site is slow,” you can say “CF-Cache-Status: BYPASS on /api/cart with Cache-Control: private from origin” — and get a fix in minutes, not days.

The Core Headers You Must Know

Not all cache headers are created equal. Some are diagnostic, some are authoritative, and some are just hints.

1. X-Cache and X-Cache-Hits (Generic / Fastly / Nginx)

X-Cache: HIT means the edge served the object without contacting origin. X-Cache: MISS means it went to origin and then cached the result (if cacheable). Some stacks return HIT, MISS when tiered caches are involved.

X-Cache-Hits: 3 counts how many times that object has been served from cache since it was stored. A value of 0 on a HIT often means the first HIT after a MISS. Fastly also exposes X-Served-By and X-Cache-Hits per PoP.

2. CF-Cache-Status (Cloudflare)

Cloudflare’s header is the most expressive. Values include HIT, MISS, BYPASS (origin said don’t cache), DYNAMIC (Cloudflare didn’t attempt caching by default), EXPIRED (was HIT but stale and revalidated), and REVALIDATED.

If you see DYNAMIC on a static asset, your Cache Rule or Page Rule is not matching. If you see BYPASS on an image, check for Cache-Control: no-cache or a Cookie header triggering bypass.

3. X-Akamai-Cache-Status / X-Cache (Akamai) and X-Cache (CloudFront)

Akamai uses X-Cache: TCP_HIT, TCP_MISS, TCP_REFRESH_HIT terminology inherited from its proxy lineage. CloudFront uses X-Cache: Hit from cloudfront or Miss from cloudfront plus Age.

Both also emit Age in seconds. Age: 0 on a HIT is suspicious — it suggests the object was just stored or is being revalidated every request.

4. Age, Cache-Control, Expires, and CDN-Cache-Control

Age: 342 tells you how many seconds the object has lived in cache. Compare it to Cache-Control: max-age=3600 to see remaining TTL: 3600 - 342 = 3258s left.

Cache-Control is the origin’s instruction to both browser and CDN. CDN-Cache-Control or Cloudflare-CDN-Cache-Control lets you set a different TTL for the edge than for the browser — essential for image optimization at the edge where you want long edge TTL but short browser TTL during redesigns.

Vary is a silent cache killer. Vary: Accept-Encoding is normal, but Vary: Cookie or Vary: * creates a cache key per cookie value, effectively making every user a MISS.

Pro tip from the field: Always inspect response headers, not request headers. Request headers like Cache-Control: no-cache from DevTools “Disable cache” will force a MISS and mislead you. Close DevTools cache disabling before measuring HIT ratio.

How to Inspect Headers in Practice

You don’t need fancy tooling to read cache status — your browser and terminal already do it.

In Chrome or Firefox, open DevTools → Network → click any request → Headers → Response Headers. Look for CF-Cache-Status, X-Cache, Age. Right-click the column headers and enable “X-Cache” as a visible column for quick scanning.

For automation and edge PoP testing, curl is the gold standard. It shows exactly what the CDN returns, without browser extensions interfering.

# Basic header-only check (follows redirects)
curl -s -I https://flyfishnwa.com/cdn-image-optimization | grep -i -E "cache|age|vary"

# Full verbose with timing and PoP info
curl -s -D - https://flyfishnwa.com/cdn-image-optimization -o /dev/null \
  -H "Cache-Control: no-cache" -w "TTFB: %{time_starttransfer}s\n"

# Force a specific edge PoP or test bypass via cookie
curl -I https://flyfishnwa.com/ -H "Cookie: session=abc123"
curl -I https://flyfishnwa.com/ -H "Pragma: no-cache"

Example HIT response you want to see on static assets:

HTTP/2 200
cache-control: public, max-age=31536000, immutable
cdn-cache-control: max-age=86400
cf-cache-status: HIT
age: 48291
x-cache: HIT
vary: Accept-Encoding

Example MISS that needs investigation on a supposedly cacheable page:

HTTP/2 200
cache-control: private, no-store
cf-cache-status: BYPASS
x-cache: MISS
set-cookie: session=xyz; Path=/; HttpOnly
vary: Cookie

For bulk auditing, use httpie or a small Bash loop. This is how we validate a deploy across 20 URLs in under 10 seconds before announcing “cache warmed.”

#!/bin/bash
URLS=(
  "/"
  "/how-cdn-works"
  "/cdn-fundamentals-guide"
  "/cdn-for-ecommerce"
)
for p in "${URLS[@]}"; do
  echo -n "$p → "
  curl -s -I "https://flyfishnwa.com$p" | grep -i "cf-cache-status\|x-cache\|age:" | tr -d '\r' | paste -sd " | " -
done

Remember that Age resets to 0 after a purge. If you purge all and immediately test, every request will be MISS — wait one second and re-request to confirm HIT. Also test from an incognito window to avoid Authorization or Cookie headers that trigger BYPASS.

Interpreting HIT, MISS, BYPASS and Friends

Reading a single header in isolation is risky. The full story comes from combining 3–4 headers together.

Header Value Meaning Typical Age Action
HIT / Hit from cloudfront Served from edge cache. No origin trip. > 0 (e.g., 1245) Healthy. Compare Age vs max-age to see freshness.
MISS / Miss from cloudfront Not in cache; fetched from origin and (if cacheable) stored. 0 or missing Expected on first request. If repeated, check Cache-Control.
BYPASS CDN intentionally skipped cache per rule or origin header. 0 Verify if bypass is intentional (e.g., cart, API).
DYNAMIC (Cloudflare) Not cacheable by default; no cache attempt made. — Add Cache Rule / CDN-Cache-Control if it should be cached.
REVALIDATED / TCP_REFRESH_HIT Stale object revalidated with origin via ETag/Last-Modified (304). 0 (reset) Normal near TTL expiry. Consider longer max-age or stale-while-revalidate.
EXPIRED Was in cache but TTL expired; fetched fresh. 0 Increase edge TTL or enable tiered cache.
STALE / HIT-STALE Served stale while revalidating in background (stale-while-revalidate). > max-age Good for performance; ensure clients tolerate slight staleness.

A common confusion is HIT with Age: 0. This is technically a HIT but indicates the object was just inserted or the CDN reset Age after a 304 revalidation. Wait 2–3 seconds and re-request — Age should climb.

Another nuance is tiered or shield caching. You might see X-Cache: MISS, HIT meaning MISS at the edge PoP but HIT at the shield PoP. That is still a win: origin was shielded. Look for X-Cache-Hits or provider-specific X-Cache-Status arrays.

For e-commerce, expect BYPASS on personalized routes. A product listing page should be HIT for anonymous users but BYPASS when a cart cookie is present. If your CDN for e-commerce setup shows HIT even with a cart cookie, you risk showing the wrong price or inventory — that is a cache-key bug.

Real-World Debugging Workflows

Theory is useful, but debugging is a checklist you run under pressure. Here are three workflows I use weekly.

Workflow 1: “Why is this page always MISS?”

Start with headers, then work backward to origin. A repeatable MISS usually means origin is forbidding caching.

curl -s -D - https://flyfishnwa.com/cdn-ssl-tls-configuration -o /dev/null

# Check 1: What does the edge say?
cf-cache-status: BYPASS
cache-control: private, max-age=0, no-store

# Check 2: What does origin say directly (bypass CDN via hosts or origin URL)?
curl -s -I https://origin.flyfishnwa.com/cdn-ssl-tls-configuration
cache-control: private, no-store  # ← culprit

# Fix at origin (Nginx example):
location ~* \.(css|js|jpg|jpeg|png|webp|avif|svg|woff2)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}
location = /cdn-ssl-tls-configuration {
  add_header Cache-Control "public, max-age=3600";
  add_header CDN-Cache-Control "public, max-age=86400";
}

After fixing, purge that URL and verify: first request MISS, second request HIT with Age > 0. If you still see BYPASS, check Cloudflare Cache Rules for a “Bypass cache” rule matching that path.

Workflow 2: “HIT but TTFB is still high”

A HIT should be 20–80ms globally. If you see HIT with 600ms TTFB, you are likely hitting a distant PoP, or the HIT is STALE with a slow revalidation, or the body is huge and uncompressed.

Inspect CF-RAY or X-Amz-Cf-Pop to see which PoP served you. Test from multiple regions with curl --resolve or WebPageTest. Also check Content-Encoding: br and Content-Length — a 4MB unoptimized image will be slow even on HIT. That is where CDN image optimization (AVIF/WebP, resizing at edge) turns a slow HIT into a fast one.

Workflow 3: “Purge didn’t work”

You purged /how-cdn-works but still see old content with Age: 12000. Two culprits: browser cache or surrogate key mismatch.

Hard-reload with curl -H "Cache-Control: no-cache" to bypass browser cache. If Age is still old, you purged the wrong key — many CDNs cache with query strings or Vary dimensions. Purge by surrogate key or use curl -X PURGE with exact URL including ?v=123. Verify with Age: 0 on next MISS.

Document every fix with a header snapshot before and after. That snapshot is your proof for post-mortems and your baseline for monitoring.

Common Pitfalls and Misconfigurations

Most cache bugs are not CDN bugs — they are header contradictions you introduced.

Pitfall 1: Setting cookies on cacheable responses. If your origin sends Set-Cookie on a static HTML page, many CDNs automatically treat it as BYPASS or private. Move cookie-setting to API endpoints only. For WordPress or similar, exclude wp-admin and logged-in cookies via Vary: Cookie bypass rules, not by sending cookies on every page.

Pitfall 2: Overly broad Vary. Vary: User-Agent fragments your cache into thousands of variants. One iPhone model gets a MISS while another gets a HIT for the same image. Keep Vary to Accept-Encoding and, if needed, Accept for content negotiation. Never Vary: * unless you truly want no caching.

Pitfall 3: Conflicting Cache-Control directives. Cache-Control: public, max-age=3600, no-cache is contradictory — no-cache wins and forces revalidation every time. Audit with curl -I after each deploy. A clean header is public, max-age=3600, stale-while-revalidate=60 for HTML and public, max-age=31536000, immutable for versioned assets.

Pitfall 4: Forgetting CDN-Cache-Control. Without it, you are forced to choose between long browser cache (bad for deployments) and short edge cache (bad for origin load). Use CDN-Cache-Control: max-age=86400 to keep edge hot while browser revalidates quickly. This pattern is covered in our CDN fundamentals guide for cache hierarchies.

Pitfall 5: HTTPS misconfigurations hiding as cache issues. Mixed content or Strict-Transport-Security errors can cause browsers to re-fetch instead of using cache. If headers look correct but browser still revalidates, check CDN SSL/TLS configuration and ensure HTTP→HTTPS redirects are cached with max-age=3600, not no-store.

Advanced: Vary, Surrogate Keys & Tiered Cache

Once basic HIT/MISS is stable, the next level is cache key design.

Every CDN builds a cache key from URL + selected headers. By default, the key is host + path + query string. Add Vary: Accept-Language and you multiply keys by language. Add Cookie and you multiply by user. The art is to keep keys narrow for static assets and precise for dynamic ones.

Surrogate keys (also called cache tags) let you purge logically. Instead of purging /product/123, /category/shoes, and /api/related separately, tag them all with Surrogate-Key: product-123 at origin and purge that tag once. Fastly and Cloudflare Enterprise expose this via Surrogate-Key and Cache-Tag headers.

# Origin sends tags
Surrogate-Key: product-123 category-shoes
Cache-Tag: product-123
CDN-Cache-Control: max-age=86400, stale-while-revalidate=300

# Purge via API (Fastly example)
curl -X POST https://api.fastly.com/service/<id>/purge/product-123 \
  -H "Fastly-Key: $FASTLY_API_TOKEN"

# Verify purge: Age should reset to 0, then HIT on second request
curl -I https://flyfishnwa.com/product/123 | grep -i age

Tiered cache (also called origin shield) adds a mid-tier PoP between edge and origin. Your header might show X-Cache: MISS, HIT — MISS at edge, HIT at shield. This still protects origin and is far better than MISS, MISS. Enable it if your origin is far from most users or under heavy load.

Finally, log these headers. Ship CF-Cache-Status, Age, and X-Cache to your observability stack. A dashboard of HIT ratio per path, per PoP, and per content type will alert you to cache regressions before users feel them. Aim for >90% HIT ratio on static assets and >70% on HTML for anonymous traffic.

Your Header Inspection Checklist

Use this 60-second checklist on every deploy or incident.

  1. Check the status first: Is it HIT, MISS, BYPASS, or DYNAMIC? If not HIT on a static asset, stop and fix Cache-Control.
  2. Read Age vs max-age: Age should be < max-age and climbing on repeated HITs. If Age: 0 on HIT, re-test in 3 seconds.
  3. Validate Cache-Control: Look for public and a sensible max-age. Remove private, no-store, or conflicting no-cache on cacheable URLs.
  4. Audit Vary and Cookies: Ensure Vary is not Cookie or * and that Set-Cookie is absent on cacheable responses.
  5. Confirm PoP and encoding: Note CF-RAY / X-Amz-Cf-Pop and Content-Encoding. A HIT without br or gzip is a missed optimization.
  6. Test anonymous vs authenticated: Compare headers with and without Cookie. Anonymous should be HIT, authenticated may correctly be BYPASS.
  7. Purge and re-verify: After any fix, purge the URL, see MISS once, then HIT with increasing Age on the next request.

Mastering these headers turns CDN debugging from guesswork into a repeatable, evidence-driven practice. Start with curl -I on your homepage today, compare it to the examples above, and you will immediately know whether your edge is working for you or just passing traffic through. For a deeper foundation, revisit how CDNs work and our beginner’s CDN fundamentals guide to connect header behavior to architecture.

Frequently Asked Questions

What does X-Cache: HIT vs MISS actually mean?

X-Cache: HIT means the CDN edge served the response from its cache without contacting your origin, so it is fast and cheap. MISS means the object was not in cache, so the edge fetched it from origin, then optionally cached it for the next request.

Why do I see CF-Cache-Status: BYPASS or DYNAMIC on Cloudflare?

BYPASS means Cloudflare intentionally skipped cache because of a Cache Rule, Page Rule, or an origin header like Cache-Control: private or Set-Cookie. DYNAMIC means the request was not considered cacheable by default and no cache rule matched — add a Cache Rule to cache it.

How do I check CDN cache headers in Chrome DevTools?

Open DevTools with F12, go to the Network tab, reload the page, click any file, and scroll to Response Headers. Look for CF-Cache-Status, X-Cache, Age, and Cache-Control. Disable “Disable cache” in DevTools while testing, or it will force MISS.

What is the Age header and how does it relate to max-age?

Age is the number of seconds the object has been in CDN cache. Compare it to Cache-Control: max-age=3600 to know remaining freshness: remaining = max-age - Age. If Age is greater than max-age, the object is stale and may be revalidated.

Why does my page show HIT but still have slow TTFB?

A HIT should be fast, but a distant PoP, huge uncompressed body, or stale-while-revalidate can still make it slow. Check CF-RAY or X-Amz-Cf-Pop for PoP location, verify Content-Encoding: br is present, and ensure images are optimized at the edge.

References & Standards: W3C Web Performance · RFC 7234