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.

Cache key mismatch: divergent keys cause separate origin fetches Two browser requests that differ only in query parameter order arrive at the CDN edge. Because the CDN does not normalize query strings, they produce different cache keys — both miss, and both reach the origin server independently. Browser A ?sort=asc&limit=20 Browser B ?limit=20&sort=asc CDN Edge Key A: /items?sort=asc &limit=20 → MISS Key B: /items?limit=20 &sort=asc → MISS Origin Server Fetch 1 ↑ (unnecessary) Fetch 2 ↑ (avoidable) Without query-string normalization, both requests miss and both hit the origin

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 (or CF-Cache-Status: HIT) from the second request onward.
  • The Age header increments consistently — a value of 0 on the second request signals a MISS and confirms the fix has not yet taken effect.
  • Fastly-Debug-Digest values 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”):

  1. Load the page and filter requests by the suspect URL pattern.
  2. Click the first request and inspect Response Headers for CF-Cache-Status: HIT or X-Cache: HIT.
  3. Hard-reload and confirm the Age value increases — a rising Age means the CDN is serving the cached copy, not reaching the origin.
  4. Open a second tab and navigate to the same URL with query parameters reordered. The Age value 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

Back to How CDN Cache Keys Are Generated