How to Debug CDN Cache Key Mismatches
Problem Statement
CDN cache key mismatches occur when two HTTP requests that should resolve to the same cached object instead generate divergent cache identifiers at the edge. The result is unnecessary cache misses, elevated origin load, and inconsistent payloads across user sessions — all without any change to the origin server.
The problem is almost never an origin outage. It originates from misaligned edge normalization, unfiltered dynamic query parameters, unstripped cookies, or conflicting Vary directives that cause the CDN to treat semantically identical requests as distinct cache entries.
Prerequisite Concepts
Before tracing a mismatch, make sure you understand how edge nodes compute identifiers in the first place. Read How CDN Cache Keys Are Generated to learn the normalization logic, query string handling, and header-casing rules that differ across CDN providers.
It also helps to understand CDN Architecture and Edge Routing Strategies as a whole — particularly origin shielding, since mismatched keys prevent origin shielding and request collapsing from working correctly. And because Vary header choices directly influence key construction, review Mapping Vary Headers to Edge Routing before making changes to your Vary configuration.
Step-by-Step Resolution
The diagram below shows how a mismatch propagates: two requests that look identical to the browser arrive at the edge with subtly different keys, resulting in separate origin fetches instead of a shared cache hit.
Step 1 — Identify low-hit-ratio endpoints
Open your CDN analytics dashboard and filter for endpoints where the cache-hit ratio is lower than expected. Requests with consistently high miss rates despite predictable, public content are the primary suspects. Export a sample of raw log lines for those URLs — you need the exact key strings the edge is computing, not just the hit/miss count.
Step 2 — Reproduce the mismatch with controlled request pairs
Send two requests that should share a cache entry but vary along one axis at a time. Start with the most common culprits:
Query string ordering:
# Request 1
curl -sI 'https://example.com/api/v2/items?sort=asc&limit=20' \
-H 'Accept-Encoding: gzip' | grep -iE 'x-cache|cf-cache-status|age'
# Request 2 — identical payload, parameters reordered
curl -sI 'https://example.com/api/v2/items?limit=20&sort=asc' \
-H 'Accept-Encoding: gzip' | grep -iE 'x-cache|cf-cache-status|age'
If Request 2 returns X-Cache: MISS (or CF-Cache-Status: MISS) and Age: 0 while Request 1 already populated the cache, you have confirmed a query-string ordering mismatch.
Trailing slash:
curl -sI 'https://example.com/docs/guide' | grep -iE 'x-cache|age'
curl -sI 'https://example.com/docs/guide/' | grep -iE 'x-cache|age'
Accept-Encoding normalization:
curl -sI 'https://example.com/bundle.js' -H 'Accept-Encoding: gzip, br' \
| grep -iE 'x-cache|age'
curl -sI 'https://example.com/bundle.js' -H 'Accept-Encoding: br' \
| grep -iE 'x-cache|age'
Step 3 — Capture the raw cache key from edge debug headers
Most CDN providers expose the computed key in a response header or log field:
| Provider | Debug header / log field |
|---|---|
| Cloudflare | CF-Cache-Status, CF-Ray; enable Cache Key via Cache Rules |
| Fastly | X-Cache, X-Cache-Hits, Fastly-Debug-Digest |
| AWS CloudFront | X-Cache, X-Amz-Cf-Pop, CloudWatch cache key logs |
| Varnish | X-Varnish, X-Cache-Key (if emitted by VCL) |
On Fastly, add Fastly-Debug: 1 to your request to receive the full cache key digest in Fastly-Debug-Digest:
curl -sI 'https://example.com/bundle.js' \
-H 'Accept-Encoding: gzip' \
-H 'Fastly-Debug: 1' \
| grep -iE 'fastly-debug-digest|x-cache'
Compare the digest values between your two variant requests. Identical digests confirm the normalization is working; different digests confirm the mismatch.
Step 4 — Apply normalization at the edge
Once you know which dimension is causing divergence, apply the appropriate fix at the CDN or origin layer.
Cloudflare Workers — strip volatile query parameters:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// Sort query parameters to canonicalize ordering
const sortedParams = new URLSearchParams(
[...url.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b))
);
url.search = sortedParams.toString();
const normalizedRequest = new Request(url.toString(), request);
return env.ASSETS.fetch(normalizedRequest);
}
};
Enable “Cache Everything” with “Ignore Query String” in Cloudflare Cache Rules for versioned static assets where the query string carries no semantic meaning.
Fastly VCL — strip volatile parameters and normalize encoding:
sub vcl_recv {
# Remove tracking and session parameters from the cache key
set req.url = querystring.filter_except(req.url, "v,id,hash,page,sort,limit");
# Normalize Accept-Encoding to a canonical set
if (req.http.Accept-Encoding ~ "br") {
set req.http.Accept-Encoding = "br";
} elsif (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} else {
unset req.http.Accept-Encoding;
}
# Enforce trailing-slash consistency
if (req.url !~ "\." && req.url !~ "/$") {
set req.url = req.url + "/";
}
}
AWS CloudFront — Cache Policy configuration:
In the AWS Console under Distributions → Behaviors → Cache Policy:
- Set “Query strings” to Include specified query strings and add only the deterministic parameters your origin uses (e.g.
v,id,hash). - Set “Headers” to Include the following headers and add only
Accept-Encoding. - Set “Cookies” to None unless your route requires session-based personalization.
Origin Cache-Control alignment:
Edge normalization is ineffective if the origin instructs the CDN not to cache at all. For fingerprinted static assets, emit:
Cache-Control: public, max-age=31536000, immutable
For dynamic routes that should be publicly cacheable with revalidation, use s-maxage to set a CDN-specific TTL independently of the browser TTL:
Cache-Control: public, max-age=60, s-maxage=3600
Sending Cache-Control: private or no-store on a response that should be publicly cached bypasses all edge normalization — the CDN will never store the response regardless of key configuration. See the guidance on combining directives safely for correct multi-tier TTL patterns.
Step 5 — Verify the fix
After deploying the normalization change, purge the affected URLs and then re-run your request pairs:
# First request — populates the cache
curl -sI 'https://example.com/api/v2/items?limit=20&sort=asc' \
-H 'Accept-Encoding: gzip' | grep -iE 'x-cache|cf-cache-status|age'
# Expected: X-Cache: MISS, Age: 0
# Wait one second, then send the reordered variant
sleep 1
curl -sI 'https://example.com/api/v2/items?sort=asc&limit=20' \
-H 'Accept-Encoding: gzip' | grep -iE 'x-cache|cf-cache-status|age'
# Expected: X-Cache: HIT, Age: 1 (or higher)
For Fastly, tail the service logs in real time to confirm key normalization:
fastly log-tail --service-id=YOUR_SERVICE_ID 2>&1 | grep -iE 'cache_key|cache_status'
Expected Output and Verification
After correct normalization is applied, every variant of an equivalent request must produce the same observable behavior:
- Both requests return
X-Cache: HIT(orCF-Cache-Status: HIT) from the second request onward. - The
Ageheader increments consistently — a value of0on the second request signals a MISS and confirms the fix has not yet taken effect. Fastly-Debug-Digestvalues are identical across all request variants.- Origin access logs show a single upstream fetch for a batch of equivalent variant requests, not one per variant.
In browser DevTools (Network tab, disable local cache with Ctrl+Shift+P → “Disable cache”):
- Load the page and filter requests by the suspect URL pattern.
- Click the first request and inspect Response Headers for
CF-Cache-Status: HITorX-Cache: HIT. - Hard-reload and confirm the
Agevalue increases — a risingAgemeans the CDN is serving the cached copy, not reaching the origin. - Open a second tab and navigate to the same URL with query parameters reordered. The
Agevalue in the second tab should be close to (but slightly larger than) the first tab’s value, confirming they share a single cache entry.
Edge Cases
Query string case sensitivity. Some CDN providers treat ?Sort=asc and ?sort=asc as distinct keys. Enforce lowercase normalization in your edge configuration before key computation runs. Fastly’s querystring.filter_except is case-sensitive by default — normalize casing explicitly with set req.url = regsub(req.url, "Sort=", "sort="); or equivalent.
HTTP/2 pseudo-header inclusion. A misconfigured edge node may include HTTP/2 pseudo-headers (:method, :path, :scheme, :authority) in the computed key. These are transport-layer constructs defined in RFC 9113 and must not participate in HTTP caching key generation as defined by RFC 9111. If your CDN logs show pseudo-headers in key strings, file a support ticket — this is a provider bug, not a configuration option.
Cookie fragmentation on public routes. Unfiltered Cookie headers included in the cache key create a unique entry per user session, producing a 100% miss rate for routes that should be publicly cached. Strip all cookies at the edge for public routes. For routes that mix public and authenticated content, use Vary: Cookie deliberately — but understand that using Vary: Accept-Encoding without fragmenting the cache requires a carefully scoped Vary set to avoid the same explosion of key variants.
Origin shield bypass. Mismatched keys prevent origin shielding and request collapsing from coalescing concurrent requests into a single upstream fetch. Until key uniformity is established, request collapsing cannot function — each unique key triggers its own shield fetch during traffic spikes. Fix the key mismatch before enabling or relying on shield-level collapsing.
Related
- How CDN Cache Keys Are Generated explains the normalization steps, query string rules, and header-casing conventions that determine what the key contains before any mismatch can occur.
- Using Vary: Accept-Encoding Without Fragmenting Cache covers how to scope
Varyheaders so encoding negotiation does not multiply your key space. - Origin Shielding and Request Collapsing shows how key uniformity is a prerequisite for effective shield-layer coalescing during traffic spikes.
- How to Combine Cache-Control Directives Safely walks through correct
max-age/s-maxagepatterns so origin responses do not accidentally opt out of CDN caching.
Back to How CDN Cache Keys Are Generated