How CDN Cache Keys Are Generated

TL;DR: A CDN cache key is the deterministic string scheme + host + path + [filtered query] + [Vary-header values] that edge nodes use to store and retrieve responses. Misconfigured keys — caused by unstripped dynamic parameters or over-broad Vary declarations — are the primary source of cache fragmentation and low hit ratios.

Quick-reference header block for publicly cacheable content:

Cache-Control: public, s-maxage=86400, stale-while-revalidate=3600
Vary: Accept-Encoding

CDN Cache Key Construction Flow A flowchart showing how an incoming HTTP request is transformed into a CDN cache key through URL normalization, query string filtering, and Vary header evaluation steps. Incoming HTTP Request e.g. GET /page?v=3 1. Normalise URL • Lowercase scheme + host • Strip default port • Resolve path (no ..) • Canonical trailing slash 2. Filter Query • Keep: v, id, hash • Strip: ts, token, sid • Sort remaining params → /page?v=3 3. Evaluate Vary Read Vary from prior cached response Append header values to key string Composite Cache Key https://example.com/page?v=3 | Accept-Encoding: gzip CDN Cache Key Construction Three normalisation stages produce the deterministic cache identifier

Mechanism & RFC Alignment

RFC 9111 §4 defines the conditions under which a stored response can satisfy an incoming request. For a shared cache (any CDN PoP), the response is only reusable if the cache key of the stored response matches the cache key of the new request. The standard mandates that any dimension listed in the Vary response header must be represented in the key — the CDN cannot serve a gzip-encoded response to a client that did not send Accept-Encoding: gzip.

Key construction begins the moment a request arrives at the edge and proceeds in strict order:

  1. Extract and normalize the base URL. The scheme is forced to lowercase, the host is canonicalized, default ports (:80 for HTTP, :443 for HTTPS) are stripped, relative path segments are resolved, and trailing-slash handling is applied consistently per the CDN’s configuration. Two requests for https://Example.COM/path/ and https://example.com/path/ must produce the same base.

  2. Apply query-string filtering. CDNs evaluate the full query string against an allowlist, denylist, or ignore-all policy configured by the operator. Parameters with deterministic meaning (v, id, hash) are retained in key order; volatile parameters (ts, token, sid, _) are stripped. The surviving parameters are typically sorted to ensure ?a=1&b=2 and ?b=2&a=1 map to the same key.

  3. Evaluate the Vary response header. For each cached response candidate, the CDN reads the Vary value returned by the origin. It then reads the corresponding request-header values from the incoming request and appends them to the composite key. This is why Vary assessment is the last step — the CDN can only know the relevant dimensions once a prior response has been stored.

Unlike browser caches, which each operate independently, CDN PoPs must construct keys that are consistent across dozens or hundreds of globally distributed nodes. A request hitting edge-us-east-1 must generate the same key as the same request hitting edge-eu-west-2, or cache topology breaks down.

Scope & Precedence

Cache keys apply exclusively to shared caches — CDN PoPs, reverse proxies, and origin shielding layers. Browser caches use their own local storage and are not governed by CDN key policy. The hierarchy:

Directive Effect on CDN key creation
public Permits the CDN to create a cache entry; key construction proceeds normally.
private Prohibits shared-cache storage entirely; no key is created at the edge.
s-maxage=N Governs how long the CDN entry (keyed by the composite key) is considered fresh.
no-store The CDN must not write a cache entry; no key persists.
no-cache A key is created, but the CDN must revalidate before serving from it.

The private directive overrides all edge key configuration — even if an operator has written Cloudflare Cache Rules to cache everything, a private response is not stored. s-maxage and max-age are distinct: s-maxage controls shared-cache TTL while max-age controls browser TTL. When both are present, s-maxage takes precedence for CDNs. The public directive is necessary for the CDN to store responses to authenticated requests (those arriving with a Cookie or Authorization header), because RFC 9111 §3.5 otherwise prohibits shared caches from caching such responses.

Implementation Patterns

Pattern 1 — Fingerprinted static assets (maximum cache lifetime)

Cache-Control: public, max-age=31536000, immutable
Vary: Accept-Encoding

The URL itself contains the content hash (/static/bundle.a4f9c2.js), so the key is naturally unique per content version. immutable signals that revalidation is unnecessary during the freshness window. Vary: Accept-Encoding is the only dimension needed because the file content is identical for all clients; only the encoding differs.

Pattern 2 — API response with shared data

Cache-Control: public, s-maxage=300, stale-while-revalidate=60
Vary: Accept-Encoding, Accept

s-maxage=300 gives the CDN a 5-minute freshness window. stale-while-revalidate=60 lets the edge serve a stale copy for up to 60 additional seconds while it revalidates asynchronously, preventing a thundering-herd origin hit at expiry. Accept is included in Vary because the endpoint serves both JSON and XML.

Pattern 3 — CDN/browser split TTL

Cache-Control: public, s-maxage=86400, max-age=300
Vary: Accept-Encoding

The CDN retains the response for 24 hours (s-maxage=86400) while browsers refresh every 5 minutes (max-age=300). This pattern is useful when content changes infrequently but you want browsers to pick up updates within minutes. The CDN key incorporates the URL and Accept-Encoding value; the browser cache is keyed separately by the browser’s own implementation.

Pattern 4 — Authenticated response (user-specific)

Cache-Control: private, no-cache, must-revalidate

No CDN cache key is created. The private directive ensures the CDN passes through to origin on every request. If the API route has a public portion (e.g. a dashboard header), split it into a separate endpoint that can use public, s-maxage.

Pattern 5 — Vary fragmentation disaster

Cache-Control: public, s-maxage=3600
Vary: User-Agent

This is the most destructive configuration in production. User-Agent has thousands of distinct values; each generates a separate CDN cache entry for the same URL. Hit ratios collapse to near zero. Replace with Vary: Accept-Encoding and perform user-agent detection server-side if you need to serve different content.

Server & CDN Configuration

Nginx

location ~* \.(js|css|woff2)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header Vary "Accept-Encoding";
    gzip_static on;
}

location /api/ {
    add_header Cache-Control "public, s-maxage=300, stale-while-revalidate=60";
    add_header Vary "Accept-Encoding, Accept";
}

Apache


    Header set Cache-Control "public, max-age=31536000, immutable"
    Header set Vary "Accept-Encoding"



    Header set Cache-Control "public, s-maxage=300, stale-while-revalidate=60"
    Header set Vary "Accept-Encoding, Accept"

Cloudflare Cache Rules

Cloudflare by default includes the full query string in the cache key. To strip volatile parameters:

  1. In the Cloudflare dashboard navigate to Caching → Cache Rules.
  2. Create a rule matching http.request.uri.path contains "/api/".
  3. Set Cache eligibility to “Eligible for cache”.
  4. Under Cache key, enable Query string → Ignore specific query string parameters and list ts, token, _.
  5. Deploy the rule.

For fingerprinted static assets, set Cache eligibility to “Eligible for cache” with TTL set to “1 year” and enable Browser TTL override to match your max-age directive.

Cloudflare also exposes the CF-Cache-Status response header, which reports HIT, MISS, EXPIRED, or BYPASS for every request — use this header to verify key configuration without needing origin log access.

Vary and key dimensionsVary is covered in depth on the Mapping Vary Headers to Edge Routing page. In the context of key generation, Vary is the primary mechanism by which the response controls key dimensions rather than CDN operator configuration. Prefer the narrowest possible Vary value: Accept-Encoding is nearly always sufficient; adding Accept-Language multiplies cache entries by the number of locale values your users send.

s-maxage and key freshness — A CDN key entry exists until the response stored under it expires (s-maxage), is purged via tag-based invalidation (covered on the Tag-Based Cache Invalidation Patterns page), or is evicted by the CDN’s LRU policy. The key itself never expires independently of the response it points to.

public vs private and public-vs-private cache scope — The public directive is the gate that allows a CDN key to be created at all for requests bearing cookies or authorization headers. Without it, RFC 9111 §3.5 prohibits the shared cache from storing the response.

Request collapsing — When a cache key has no stored entry (a cold MISS), multiple concurrent requests for the same URL will attempt to fetch from origin simultaneously. Origin shielding and request collapsing relies on key determinism: if any two concurrent requests generate different keys, they cannot be collapsed into a single upstream fetch, doubling or tripling origin load during traffic spikes.

Verification Workflow

Step 1 — Confirm the CDN is creating a cache entry

# First request — expect MISS and Age: 0
curl -sI https://example.com/static/bundle.a4f9c2.js \
  | grep -iE 'cache-control|vary|cf-cache-status|x-cache|age'

# Second request — expect HIT and Age > 0
curl -sI https://example.com/static/bundle.a4f9c2.js \
  | grep -iE 'cf-cache-status|x-cache|age'

If CF-Cache-Status or X-Cache shows MISS on both requests, the response is either private, no-store, or the query string contains a unique parameter per request.

Step 2 — Test that query-string ordering does not fragment the key

curl -sI 'https://example.com/api/items?sort=desc&limit=20' \
  | grep -iE 'cf-cache-status|x-cache|age'

curl -sI 'https://example.com/api/items?limit=20&sort=desc' \
  | grep -iE 'cf-cache-status|x-cache|age'

Both requests should return the same Age value. If Age differs, the CDN is not normalizing query-string parameter order.

Step 3 — Verify Vary dimensions are correct

# Send a request with a specific encoding
curl -sI -H 'Accept-Encoding: gzip' https://example.com/page \
  | grep -iE 'vary|content-encoding|cf-cache-status|age'

# Send without encoding — should create a separate cache entry
curl -sI https://example.com/page \
  | grep -iE 'vary|content-encoding|cf-cache-status|age'

A Vary: Accept-Encoding response with gzip will have Age: 0 on the second request (no encoding) because this is a legitimately different cache entry.

Step 4 — Browser DevTools

  1. Open DevTools → Network, disable cache in the Network settings.
  2. Reload the target resource twice.
  3. On the second load, confirm Age is increasing and CF-Cache-Status reads HIT.
  4. Check that Vary in the response headers lists only Accept-Encoding (not User-Agent, Cookie, or Authorization).

Failure Modes & Gotchas

  1. Unstripped nonce or timestamp parameters. A URL like /api/data?_t=1718000000 generates a unique key per millisecond. Strip _t, ts, _, rand, and similar parameters unconditionally at the CDN edge.

  2. Cookie forwarding enabled. Many CDN configurations forward Cookie headers to origin for backend personalization. If the Cookie header is also included in the cache key (the Fastly default unless overridden), every user session produces a unique key and hit ratio falls to zero.

  3. Vary: * — The asterisk wildcard means no response under this URL can ever match a subsequent request. The CDN must treat every request as a cache miss. This is occasionally set by origin frameworks erroneously. Identify it with curl -sI <url> | grep Vary and fix it at the origin.

  4. Trailing-slash inconsistency. /about and /about/ are distinct cache keys. Enforce a canonical form at the CDN or origin router and redirect non-canonical URLs before they reach the cache lookup.

  5. HTTP/2 pseudo-headers in CDN logs. Some edge implementations log :path and :authority pseudo-headers alongside the cache key, which can make the key appear different from what the HTTP/1.1 normalization logic computes. Verify using the CDN’s native cache-key debug header rather than log inference.

  6. Origin returns a different Vary after a cache miss. If the origin changes its Vary value between the first and second request (e.g. during a deploy), the CDN may store two entries for the same URL under different key dimensions. Purge the affected URLs after deploying changes to Vary.

  7. Protocol mismatch. HTTP and HTTPS produce separate cache keys because the scheme is part of the base URL. Ensure all traffic is redirected to HTTPS before the CDN cache lookup, otherwise HTTP requests never benefit from the HTTPS-keyed entries.

  8. Range requests. A Range: bytes=0-1023 request generates a partial response (206 Partial Content) that is typically stored under a different key from the full response, even for the same URL. Avoid range-request caching unless you have explicit CDN support for it (Cloudflare supports byte-range caching as a paid feature).

FAQ

What is the difference between a CDN cache key and an ETag?

A cache key is the identifier the CDN uses to locate a stored response. An ETag (or Last-Modified) is a validator the CDN uses to check whether that stored response is still current when revalidating with the origin. The key and the validator are orthogonal: a response can have a stable key but a stale ETag.

Can I include a custom request header in the CDN cache key?

Yes. Cloudflare Cache Rules allow you to add custom request header values to the cache key (“Custom Cache Key”). Fastly VCL exposes bereq.http.<header> for the same purpose. Only include headers that carry meaningful content variation — including high-cardinality headers like X-Forwarded-For will fragment the cache the same way Vary: User-Agent does.

Does HTTP/2 or HTTP/3 change cache key construction?

No. HTTP/2 and HTTP/3 are transport-layer protocols. The semantic caching model defined in RFC 9111 applies identically across all HTTP versions. Cache key construction is based on the HTTP method, URL, and response Vary header — none of which are affected by the underlying transport.

Why does my CDN show a HIT on the first request?

The response was already stored by a prior request from a different client, from a background prefetch crawler, or from origin-shield warming. A HIT on the first request from your client is expected behavior for a well-warmed CDN and indicates the key is being shared correctly across clients.

How do I verify which headers are included in the Vary dimension of a specific CDN entry?

On Cloudflare, add CF-Cache-Status and inspect the Vary response header. For Fastly, use the Fastly-Debug: 1 request header to receive X-Cache-Key and Vary fields in the response. On Varnish, varnishlog -q "RespHeader:Vary" streams the Vary values for every transaction.


Back to CDN Architecture & Edge Routing Strategies