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; requests with different keys are cached independently. Getting cache-key design right is critical: too narrow a key fragments the cache and destroys hit ratio; too broad a key means different users receive each other’s personalised or content-negotiated responses — a correctness and 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 — instructs the CDN to add specific request headers to the key.
  • Custom cache key rules — CloudFront, Fastly, and Cloudflare all allow adding cookies, custom headers, or stripping query parameters from the key.
  • Normalisation — some CDNs normalise query-parameter order; others do not, resulting in /api?a=1&b=2 and /api?b=2&a=1 being cached separately.

Query strings are the most common source of unintended cache fragmentation. An analytics or session parameter (e.g. ?utm_source=email&_ga=2.12345) added by marketing tools causes every URL variant to be cached separately, effectively defeating the CDN for that resource.

Send the same URL with different query strings and check whether all responses are served from cache:

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 is including 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 is true: each query variation must be cached 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 warming both URLs, verify that each returns independent cached responses — and that page=1 does not receive the same body as page=2.

The Vary response header tells the CDN which request headers to add to the cache key. A response with Vary: Accept-Encoding means compressed and uncompressed versions are cached separately. A response with Vary: User-Agent means every browser variant gets its own cached copy — an effective cache-killer.

Vary valueImpact
Vary: Accept-EncodingCorrect for text assets. Compresses content is cached separately from uncompressed.
Vary: Accept-LanguageMay be appropriate for internationalised responses, but fragments 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 effectively eliminate caching.
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 and assert that the same cacheable asset is served as a hit regardless of those header values (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 — a critical misconfiguration for a static asset.

Cache poisoning is a security vulnerability where an attacker causes malicious content to be stored in the CDN cache and served to other users. While MaxoPerf is a load testing tool (not a security scanner), CDN load tests should be designed with awareness of cache poisoning vectors to avoid accidentally triggering them.

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 can control that header can poison the cache. When testing, do not send unusual headers to production CDNs unless explicitly testing for 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 be served a cached response for /script.js — or could create a new cache entry accessible at an unexpected URL.
  • Host header injectionHost header manipulation can cause cross-site content to be cached. MaxoPerf load tests should always send the correct Host header for the CDN endpoint.

Some CDNs normalise query-parameter order before building the cache key; others do not. If your CDN does not normalise, /api?a=1&b=2 and /api?b=2&a=1 will be cached independently, halving 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.
  • Document your CDN’s cache-key rules explicitly — undocumented variations are the root cause of 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.