High CDN Cache Miss Rate
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://ipfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://ipfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://ipfyi.com/entity//)
Use the native HTML custom element.
A CDN is deployed in front of an origin server, but cache hit rates are unexpectedly low — often below 50%. Nearly every request passes through to the origin, negating the CDN's latency and cost benefits. The issue is usually caused by misconfigured cache headers, query string proliferation, cookie variation, or cache key configuration problems that cause the CDN to treat effectively identical requests as unique.
Symptoms
- ⚠ CDN analytics dashboard shows cache hit ratio below 50%, often much lower
- ⚠ Origin server CPU and bandwidth remain high despite CDN deployment
- ⚠ Response headers show 'cf-cache-status: MISS' or 'x-cache: MISS' on repeated requests for the same URL
- ⚠ Time to First Byte (TTFB) remains high globally, indicating CDN is not serving from edge
- ⚠ CDN billing shows high origin egress charges disproportionate to the number of unique assets
- ⚠ Purging or invalidating the cache has no measurable effect on performance, since content was never cached
Possible Root Causes
- • Origin sends 'Cache-Control: private' or 'Cache-Control: no-store' on responses that are identical for all users, preventing CDN caching entirely
- • Vary: Cookie header causes the CDN to create a unique cache entry per session cookie, turning a cacheable resource into millions of unique cache keys
- • Cache-busting query strings generated with timestamps or random values on every request fragment the cache; CDN sees each as a distinct URL
- • Very short max-age values (1-30 seconds) cause constant cache eviction, resulting in near-100% miss rate under normal TTL-based caching semantics
- • CDN cache key includes headers or cookies that vary between users (User-Agent, Accept-Language, session tokens) when the response is actually the same for all
Diagnosis Steps
Step 1 — Check cache status headers on live responses
# Inspect CDN cache status header on the same URL repeated multiple times
for i in $(seq 1 5); do
curl -sI https://your-domain.com/page | grep -i 'cf-cache-status\|x-cache\|cache-control\|age\|vary'
echo "---"
done
# A cache HIT should return 'age' > 0 and cf-cache-status: HIT
# A MISS on every request = caching is not working
Step 2 — Analyse Cache-Control and Expires headers from origin
# Check what cache directives the origin is sending
curl -sI https://your-domain.com/api/data | grep -i 'cache-control\|pragma\|expires\|set-cookie\|vary'
# Common problems:
# Cache-Control: no-store -> CDN cannot cache at all
# Cache-Control: private -> CDN will not cache (designed for browsers only)
# Cache-Control: no-cache -> CDN must revalidate on every request
# Vary: Cookie -> CDN creates a separate cache entry per cookie value
# Set-Cookie: session=... -> CDN treats each unique cookie as a unique cache key
Step 3 — Check if query strings are causing cache fragmentation
# Count unique query strings for a URL that should be a single cacheable resource
# Check access logs on origin
grep "/assets/app.js" /var/log/nginx/access.log | \
awk '{print $7}' | sort | uniq -c | sort -rn | head -20
# If the same file has dozens of query string variants, the CDN creates separate cache entries
# Common offenders: ?_=<timestamp>, ?v=<random>, ?utm_source=...
Step 4 — Review CDN cache key configuration
# For Cloudflare, check Page Rules or Cache Rules for the URL
# Look for whether query strings are included in the cache key
curl -sI "https://your-domain.com/static/style.css?v=1234" | grep cf-cache-status
curl -sI "https://your-domain.com/static/style.css?v=5678" | grep cf-cache-status
# If both MISS, CDN is using query strings in the cache key for this URL
Step 5 — Check TTL (Time-To-Live) values
# A very short TTL causes high eviction rates even if items are cached
curl -sI https://your-domain.com/page | grep -i 'cache-control\|age'
# If max-age=1 or max-age=5, the CDN must re-fetch very frequently
# Check Cloudflare's edge cache TTL in the dashboard vs. what origin sends
# Measure age header to confirm content is being cached
curl -sI https://your-domain.com/page | grep -i age
Step 6 — Profile the CDN logs for cache status distribution
# If you have access to CDN logs (e.g., Cloudflare Logpush → S3):
# Count HIT vs MISS vs EXPIRED vs BYPASS vs DYNAMIC
jq -r '.CacheStatus' cdn-logs.json | sort | uniq -c | sort -rn
# Or via Cloudflare Analytics API
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/analytics/dashboard" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" | python3 -m json.tool | grep -A5 -i cache
Solution
Fix 1 — Correct Cache-Control headers on origin
Set appropriate Cache-Control directives for each resource type:
# nginx example — static assets: long TTL, immutable
location /static/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTML pages: short TTL, revalidation
location / {
add_header Cache-Control "public, max-age=60, stale-while-revalidate=600";
}
# API responses that are the same for all users
location /api/public/ {
add_header Cache-Control "public, max-age=300, s-maxage=3600";
# s-maxage overrides max-age for CDN (shared caches), allowing longer CDN TTL
# max-age is still 5 minutes for browsers
}
# User-specific content: must not be cached by CDN
location /api/user/ {
add_header Cache-Control "private, no-store";
}
Fix 2 — Remove or normalise Vary: Cookie
If an origin sends Vary: Cookie for public content:
# Remove Set-Cookie header from cacheable responses
# (Only appropriate if the cookie is not used for the response content)
proxy_hide_header Set-Cookie;
proxy_ignore_headers Set-Cookie;
add_header Cache-Control "public, max-age=300";
For Cloudflare, use a Cache Rule to strip specific cookies from the cache key:
# Cloudflare Dashboard > Caching > Cache Rules
# Match: hostname = your-domain.com AND path starts with /static/
# Cache eligibility: eligible for cache
# Cache key: ignore all cookies (or specify cookies to ignore)
Fix 3 — Normalise cache-busting query strings
If your application appends dynamic query strings to cacheable assets:
# For Cloudflare: use Cache Key rules to strip tracking parameters
# Cloudflare Dashboard > Caching > Cache Keys (Enterprise) or use Transform Rules
# For nginx as CDN origin:
# Strip known non-functional query params before proxying
location /static/ {
# Ignore cache-busting params in upstream cache key
proxy_cache_key "$host$uri"; # Omit $args to ignore query strings
proxy_pass http://backend;
proxy_cache my_cache;
}
Fix 4 — Increase edge cache TTL for rarely-changing content
# Set a longer s-maxage for CDN while keeping browser TTL short (optional)
add_header Cache-Control "public, max-age=60, s-maxage=86400";
# Or use Cloudflare's "Browser Cache TTL" and "Edge Cache TTL" settings separately
# Dashboard: Caching > Configuration > Browser Cache TTL
Fix 5 — Verify fix with cache status monitoring
# After applying changes, monitor cache hit rate over time
watch -n 5 'curl -sI https://your-domain.com/page | grep -i "cf-cache-status\|age"'
# Check age header increases on subsequent requests (confirming cache is being used)
Prevention
- Establish a cache header policy in your application framework that sets appropriate Cache-Control directives by response type (static, public API, private API) and apply it via middleware
- Add cache status headers to your monitoring dashboard and set alerts when the CDN hit rate drops below 80% for static assets
- Use versioned asset URLs (e.g., /static/app.v1234.css) with long max-age instead of cache-busting query strings to avoid cache fragmentation
- Regularly audit Vary headers in HTTP responses — Vary: Cookie or Vary: User-Agent on public resources is a common mistake that silently kills cache efficiency
- Test cache behaviour as part of deployment checks: verify that repeated requests to key URLs return cf-cache-status: HIT within one TTL cycle