Performance

Cache and CDN: the real hit rate, measured

Of 60 requests, 40 came from the edge cache (66.7%): static assets hit 40 of 40, HTML 0 of 20. TTFB goes from 26 to 276 ms.

Key points

  • Of 60 measured requests, 40 came from the edge cache: 66.7% overall, 100% on static assets.
  • HTML answers DYNAMIC with Cache-Control: no-cache on all 20 requests.
  • Median TTFB is 26-34 ms for assets and 256-276 ms for HTML: the difference is the origin.
  • The query string is part of the cache key: a new URL misses, the second request hits.

Of 60 requests to the site, 40 were served from the edge cache, 66.7%: all 40 static assets hit, all 20 HTML requests came back DYNAMIC. Median TTFB goes from 26-34 ms for assets to 256-276 ms for HTML.

What the edge answers

ResourceRequestsHITMedian TTFBCache-Control
Home HTML100276 msno-cache
Article HTML100256 msno-cache
CSS101026 msmax-age=2592000, swr=604800
Font101034 msmax-age=31536000, immutable
Image101030 msmax-age=2592000
Icon101029 msmax-age=2592000

The gap between 30 and 276 ms is the cost of the origin: HTML is generated on every request, assets come from the edge. That is not a CDN defect, it is a decision: HTML changes often and has no stable cache key.

Measuring it from a terminal

for u in / /blog/core-web-vitals-ttfb /css/components-ui.css \
         /fonts/inter-400-latin.woff2; do
  printf '%s ' "$u"
  curl -sI "https://ui.eneawork.it$u" |
    grep -i -E '^(cf-cache-status|age|cache-control):' | tr '\n' ' '
  echo
done

The command prints cache status, object age and cache policy for each URL. Age says how many seconds the edge has held that copy: 83,580 s for the CSS, almost a day.

The query string is part of the key

With a new URL (?probe=...) the first answer is MISS and the second is HIT: the parameter is part of the key. A campaign identifier that differs per user empties the cache on every visit, and so does an ordering parameter in internal links.

What to change

  • Assets with a hash in the name: a one-year cache and immutable, like the font answering with max-age=31536000. Source: web.dev, CC BY 4.0.
  • HTML: when you cannot cache it, cut the origin TTFB. In the local test the home page answers in 205 ms. Source: web.dev, CC BY 4.0.
  • Query strings: normalise the parameters that do not change the content, otherwise every variant is a new object. Source: MDN, CC BY-SA 2.5.
  • stale-while-revalidate=604800 serves the old copy at once and refreshes it afterwards: the site CSS already uses it.

The limitation to name

The measurement comes from one client and one point of presence (Milan, from the CF-RAY), with ten requests per URL. Real hit rate depends on traffic, geography and TTLs: a site with many HTML pages and few assets shows a much lower figure, because the 66.7% here mixes two different populations.

Where to continue

The EneUI catalogue indexes 111,985 components, for instance card-stack, and the grid of 1,556 blocks: every record is an HTML page that never reaches the edge cache. Read how to measure TTFB, caching with a service worker and how to set a performance budget.

Frequently asked questions

What is cache hit rate?

It is the share of requests served from cache instead of the origin. In our sample it is 66.7% overall and 100% on static assets alone.

Why is HTML not cached?

Because it answers with Cache-Control: no-cache and cf-cache-status DYNAMIC. The content changes often and the edge has no stable key to reuse it.

How do I read cf-cache-status?

HIT means served from the edge, MISS means fetched from the origin and stored, DYNAMIC means not cacheable under the current rules, EXPIRED means the stored copy ran out.

Is the TTFB of a HIT always low?

It depends on the distance to the point of presence. Here 26-34 ms from the edge against 256-276 ms from the origin, measured from the same client.

Does a query string break the cache?

Yes, when it is part of the key. A URL with a new parameter is a new object: the first request misses, the second hits.