Improve Your CDN Cache Hit Ratio
21 Jan 2026A low cache hit ratio means your CDN is a slow, expensive relay to your origin server instead of the thing that’s supposed to protect it. Most of the time the cause is one of a handful of header or config mistakes, not a fundamental limitation of the CDN.
Check the ratio first, then the reason
Every major CDN (Cloudflare, Fastly, CloudFront) exposes a cache hit ratio in its analytics dashboard. Look at it broken down by path or content type, not just as one aggregate number — a 95% hit ratio on static assets can hide a 5% hit ratio on your HTML, and the aggregate number won’t tell you that.
Cause 1: Cache-Control headers that forbid caching
The most common reason a CDN keeps going to origin: your server is telling it not to cache.
Cache-Control: private
Cache-Control: no-store
Set-Cookie: session=...
Any of these on a response the CDN sees will usually prevent caching — private explicitly excludes shared caches, no-store excludes everyone, and many CDNs (correctly, by default) won’t cache a response that sets a cookie, since that usually signals per-user content. If you’re serving genuinely public content but your framework is attaching a session cookie to every response regardless (a common default in several frameworks), strip it for cacheable routes or the CDN will treat every response as private.
Cause 2: Query strings fragmenting the cache
By default, most CDNs treat /product?id=5 and /product?id=5&utm_source=twitter as different cache keys — same content, two entries, two origin fetches. If your traffic sources add tracking params (utm_source, fbclid, gclid), your effective hit ratio for that page tanks even though the content never actually changes.
Configure your CDN’s cache key rules to ignore params that don’t affect the response:
# Cloudflare example: Cache Rules → "Cache Key" → exclude query params
# Ignore: utm_source, utm_medium, utm_campaign, fbclid, gclid
Or normalize/strip them at the origin before generating the response, so the CDN key is consistent regardless of how the visitor arrived.
Cause 3: Vary header doing more than it should
Vary: User-Agent or Vary: Accept-Encoding tells the CDN to cache a separate copy per distinct header value. Accept-Encoding is fine — there are only a few real values (gzip, br, identity). User-Agent is a disaster — there are effectively unlimited distinct user agent strings, so Vary: User-Agent fragments your cache into nearly one entry per visitor, which is close to no caching at all.
# Fine
Vary: Accept-Encoding
# Fragments your cache badly
Vary: User-Agent
Remove Vary: User-Agent unless you’re genuinely serving different bytes per user agent (rare — usually you’re doing that client-side with JS, not server-side).
Cause 4: TTL too short for content that barely changes
If max-age is set to 60 seconds on content that changes once a day, you’re forcing a re-fetch from origin every minute regardless of whether anything changed. Match the TTL to actual change frequency, and use stale-while-revalidate to serve the cached copy while refreshing in the background instead of making a user wait on a cache miss:
Cache-Control: public, max-age=300, stale-while-revalidate=3600
This serves the 5-minute-old cached copy instantly while the CDN quietly re-fetches in the background — the user never waits on the miss.
Cause 5: origin errors treated as uncacheable
If your origin occasionally returns a 500 and your CDN is configured to never cache error responses (usually the right default), a flaky origin under load creates a feedback loop: errors bypass cache, which increases origin load, which causes more errors. Fix the origin reliability issue directly — don’t try to cache around it, since caching a 500 for the wrong duration risks serving an error page to users after the origin recovers.
The takeaway
Check your hit ratio by path, not in aggregate. The usual culprits are private/cookie-triggered no-cache on public content, query string fragmentation, an overzealous Vary header, and TTLs shorter than the content’s real change frequency. Fix the header, not the CDN config, in most cases.