Skip to content

Cache hit/miss testing

Cache hit ratio is the basic health metric for a CDN. A high hit ratio means the edge serves most requests without touching your origin. A low hit ratio means your Cache-Control headers are misconfigured, your cache keys are too narrow, or your content changes faster than users can warm the cache. This page shows how to measure hit ratio, assert on cache-state headers, and compare warm vs cold cache latency in MaxoPerf.

  • Read the CDN testing overview to see all the CDN test types.
  • Confirm that your CDN returns Cache-Control, Age, or X-Cache response headers. Run curl -I https://your-cdn-url/asset.js to check before you write a test.
  • Get a smoke test passing against your CDN URL first.

Before you write assertions, learn what each header means:

HeaderSourceWhat it tells you
Cache-ControlOrigin serverCaching directives: max-age, s-maxage, no-cache, no-store, public, private.
AgeCDNSeconds the response has been in the cache. Age: 0 on the first hit; increasing on subsequent hits. A missing or zero Age on repeated requests suggests the CDN is not caching the response.
X-CacheCDN (provider-specific)HIT or MISS. CloudFront uses X-Cache: Hit from cloudfront. Fastly uses X-Varnish-Cache. Akamai uses X-Cache: TCP_HIT. Check your provider’s documentation.
X-Cache-HitsFastly / some CDNsNumber of times this edge has served the cached response. Use it to confirm warm state.
Cf-Cache-StatusCloudflareHIT, MISS, EXPIRED, STALE, BYPASS, DYNAMIC. Very granular.

The most useful CDN test separates two phases:

  1. Cold run: send one request per URL to prime the cache. Expect MISS on all responses. Record latency. This is your origin baseline.
  2. Warm run: send concurrent load after the cache is primed. Expect HIT on most responses. Record latency. This is your edge performance.

The ratio of warm to cold latency shows what the cache is worth. If warm TTFB is 20 ms and cold is 200 ms, the CDN absorbs 90 % of the cost.

The following Taurus YAML runs a warm-cache load test and asserts that:

  • The HTTP status code is 200.
  • The response contains a Cache-Control header with public.
  • The response Age is greater than zero (i.e., served from cache, not origin).
execution:
- concurrency: 50
ramp-up: 30s
hold-for: 3m
scenario: cdn-warm-cache
scenarios:
cdn-warm-cache:
default-address: https://cdn.example.com
requests:
- label: homepage-js-bundle
url: /static/app.js
method: GET
assert:
- equals:
subject: http-code
value: '200'
- contains:
subject: headers
value: 'public' # Cache-Control: public, max-age=...
- not-contains:
subject: headers
value: 'no-store' # guard against accidental no-store
- label: hero-image
url: /static/hero.webp
method: GET
assert:
- equals:
subject: http-code
value: '200'
- contains:
subject: headers
value: 'X-Cache: HIT' # adjust to your CDN's header name
- label: api-response-cached
url: /v1/catalog/featured
method: GET
assert:
- equals:
subject: http-code
value: '200'

k6 gives you precise per-header checks and lets you record cache state for custom reporting:

import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 50 },
{ duration: '3m', target: 50 },
{ duration: '30s', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<100'], // CDN hits should be fast
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get('https://cdn.example.com/static/app.js', {
tags: { name: 'cdn-asset' },
});
check(res, {
'status 200': (r) => r.status === 200,
'cache hit': (r) => (r.headers['X-Cache'] || '').includes('HIT'),
'cache-control public': (r) => (r.headers['Cache-Control'] || '').includes('public'),
'age is positive': (r) => parseInt(r.headers['Age'] || '0', 10) > 0,
});
sleep(0.5);
}

Run two separate MaxoPerf tests to compare:

  1. Cold test: one VU, one pass through all asset URLs. Note the average TTFB. This is origin RTT.
  2. Warm test: after the cold priming test finishes, ramp to production VU count. Note p50 and p95 TTFB.

After your run, open the Overview tab and look at:

SignalWhat it means
p50 TTFB < 30 msA nearby edge node serves the responses.
p50 TTFB > 100 msMost responses come from origin. Check the Age header assertions. They may be failing.
Assertion failure rate > 0 %Some responses bypass the cache. Drill into the Log tab to see which URLs failed which assertion.
High p99 relative to p50Occasional cache misses cause long-tail latency spikes. TTL may be expiring mid-test.

Do:

  • Warm the cache before measuring steady-state hit latency.
  • Label requests by asset type or URL group. The per-label breakdown in MaxoPerf then shows which resource category has the worst hit ratio.
  • Use assertion failure rate as a proxy for cache miss rate when X-Cache headers are available.

Don’t:

  • Measure cache performance from a single geographic location. Edge nodes are regional. A HIT in us-east-1 does not mean a HIT in eu-west-1. See Multi-region edge performance.
  • Confuse a 200 response with a cache hit. The CDN may return 200 from origin on every miss. You need the X-Cache or Age header to distinguish.
  • Forget to check the Vary header on responses. A misconfigured Vary: User-Agent can split the cache into per-client copies with a near-zero hit ratio. See Cache key and Vary testing.