Skip to content

Cache key and Vary testing

The cache key is the string a CDN uses to look up a cached response. Two requests with the same cache key get the same cached response. The CDN caches requests with different keys separately. Cache-key design matters. A key that is too narrow fragments the cache and wrecks the hit ratio. A key that is too broad sends one user’s personalised or content-negotiated response to another user, which is both a correctness and a security problem.

By default, most CDNs build the cache key from:

  • Scheme (https://)
  • Host (cdn.example.com)
  • Path (/static/app.js)
  • Query string (optionally, and often configurable)

Extensions to the key depend on CDN configuration:

  • Vary header: tells the CDN to add specific request headers to the key.
  • Custom cache key rules: CloudFront, Fastly, and Cloudflare all let you add cookies or custom headers to the key, or strip query parameters from it.
  • Normalisation: some CDNs normalise query-parameter order. Others do not, so they cache /api?a=1&b=2 and /api?b=2&a=1 separately.

Query strings are the most common cause of unintended cache fragmentation. Marketing tools add analytics or session parameters (e.g. ?utm_source=email&_ga=2.12345). The CDN then caches every URL variant separately, and the CDN stops helping for that resource.

Send the same URL with different query strings and check whether the cache serves all responses:

scenarios:
query-string-test:
requests:
# Clean URL — establishes cache entry
- label: clean-url
url: /static/app.js
method: GET
# UTM parameter — should hit same cached entry if CDN ignores UTM params
- label: utmed-url
url: /static/app.js?utm_source=email&utm_campaign=launch
method: GET
assert:
- contains:
subject: headers
value: 'X-Cache: HIT' # should be a cache hit if UTM is stripped
# Session parameter — should NOT vary the cache if caching is anonymous
- label: session-url
url: /static/app.js?session_id=abc123
method: GET
assert:
- contains:
subject: headers
value: 'X-Cache: HIT'

If the UTM-parameterised URL returns a MISS but the clean URL returns a HIT, your CDN includes query parameters in the cache key. Configure the CDN to ignore tracking parameters for static assets.

Test for required query-string differentiation

Section titled “Test for required query-string differentiation”

For API responses that use query parameters to vary content, the opposite holds. The CDN must cache each query variation separately:

scenarios:
api-cache-key-test:
requests:
# Different page numbers must return different cached responses
- label: page-1
url: /v1/catalog?page=1&limit=20
method: GET
assert:
- equals:
subject: http-code
value: '200'
- label: page-2
url: /v1/catalog?page=2&limit=20
method: GET
assert:
- equals:
subject: http-code
value: '200'

After you warm both URLs, verify that each returns its own cached response, and that page=1 does not get the same body as page=2.

The Vary response header tells the CDN which request headers to add to the cache key. With Vary: Accept-Encoding, the CDN caches compressed and uncompressed versions separately. With Vary: User-Agent, every browser variant gets its own cached copy, which all but disables the cache.

Vary valueImpact
Vary: Accept-EncodingCorrect for text assets. The CDN caches compressed content separately from uncompressed.
Vary: Accept-LanguageMay fit internationalised responses, but fragments the cache heavily.
Vary: CookieCatastrophic for any public-facing resource. Every user’s cookies produce a unique cache entry.
Vary: User-AgentCatastrophic. Thousands of User-Agent strings mean almost nothing gets cached.
Vary: *Disables caching entirely for that response. Usually a configuration error.

Write a test that sends requests with different Accept-Language or User-Agent values. Assert that the CDN serves the same cacheable asset as a hit whatever those header values are (if the content is the same across languages or clients):

scenarios:
vary-test:
requests:
- label: default-agent
url: /static/app.js
method: GET
headers:
User-Agent: 'Mozilla/5.0 (compatible; load-test)'
assert:
- contains:
subject: headers
value: 'X-Cache: HIT'
- label: different-agent
url: /static/app.js
method: GET
headers:
User-Agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)'
assert:
- contains:
subject: headers
value: 'X-Cache: HIT'

If different-agent returns a MISS when default-agent returned a HIT, the CDN has Vary: User-Agent active on that response. For a static asset, that is a critical misconfiguration.

Cache poisoning is a security vulnerability. An attacker gets malicious content stored in the CDN cache, and the CDN serves it to other users. MaxoPerf is a load testing tool, not a security scanner. Still, design your CDN load tests with cache poisoning vectors in mind so you do not trigger them by accident.

Common cache poisoning patterns to be aware of

Section titled “Common cache poisoning patterns to be aware of”
  • Unkeyed request headers: if a CDN includes an X-Custom-Header in cached responses but not in the cache key, an attacker who controls that header can poison the cache. Do not send unusual headers to production CDNs unless you are testing for exactly this.
  • URL normalisation differences: some CDNs normalise paths (//static/app.js → /static/app.js) before caching but serve responses that reflect the original path. A request for //script.js could get the cached response for /script.js, or could create a new cache entry at an unexpected URL.
  • Host header injection: a manipulated Host header can get cross-site content cached. Always send the correct Host header for the CDN endpoint in MaxoPerf load tests.

Some CDNs normalise query-parameter order before building the cache key. Others do not. If your CDN does not, it caches /api?a=1&b=2 and /api?b=2&a=1 separately, which halves the hit ratio for that endpoint.

Test for this:

scenarios:
normalisation-test:
requests:
# Forward order
- label: params-ab
url: /v1/catalog?category=shoes&limit=20
method: GET
# Reversed order — should hit the same cache entry if normalised
- label: params-ba
url: /v1/catalog?limit=20&category=shoes
method: GET
assert:
- contains:
subject: headers
value: 'X-Cache: HIT'

Do:

  • Audit Vary headers on every cached response type. Vary: Accept-Encoding is correct. Vary: User-Agent or Vary: Cookie on public assets is almost always wrong.
  • Test with both clean URLs and query-parameterised URLs to confirm the CDN is stripping (or including) query parameters as intended.
  • Write down your CDN’s cache-key rules. Undocumented variations cause most cache-fragmentation surprises.

Don’t:

  • Use Vary: * on any response that should be cached. It prevents caching entirely.
  • Assume CDN cache-key defaults are correct for your use case. Every CDN has different default query-string handling.
  • Ignore Vary header misconfigurations in load test results. A suspiciously low hit rate with high TTFB on a widely shared asset is often a Vary problem.