Content Delivery Network Blog

The Vary Header: Avoiding Cache Fragmentation on a CDN

Written by BlazingCDN | Aug 29, 2026, 7:33:00 AM

The Vary header is a multiplier on your cache key. A CDN edge stores one copy of an object per distinct combination of the request headers listed in Vary, so Vary: Accept-Encoding, Accept-Language on a catalog served in 12 locales can turn one URL into 36 or more cache entries. In a 1-million-URL model with a 24-hour TTL and eight requests per URL per TTL, that shift takes edge hit ratio from 87.5% to effectively zero and multiplies origin egress by 8x. Normalization before the cache key is computed is the fix, and it costs about ten lines of config.

How the Vary header fragments a CDN cache

Per RFC 9111, Vary defines the secondary cache key: a stored response can only be reused for a request whose listed headers match the ones on the original request. The primary key is the method plus URL. The secondary key is everything Vary names. The cache does not know which parts of those headers actually changed the response body — it matches on what it was told to match on.

That is why fragmentation is silent. Nothing errors. Requests still return 200. The edge simply keeps missing, refilling, and evicting variants that differ only in whitespace or token order, while your dashboard shows a hit ratio sliding two points a quarter and nobody can name the cause.

The fragmentation math behind a Vary header CDN miss storm

Model assumptions, stated so you can substitute your own: 1,000,000 unique URLs, mean object size 200 KB, 24-hour TTL, and eight requests per URL per TTL reaching a given edge tier (long-tail-heavy, typical of software catalogs and media libraries). Each distinct variant costs one additional origin fill.

  • No Vary: 1 miss per 8 requests → 87.5% hit ratio, ~200 GB/day origin egress.
  • Vary: Accept-Encoding, normalized to 3 buckets: 3 misses per 8 → 62.5% hit ratio, ~600 GB/day.
  • Vary: Accept-Encoding, raw client values (6 distinct strings average): 6 misses per 8 → 25% hit ratio, ~1.2 TB/day.
  • Vary: Accept-Encoding, Accept-Language across 12 locales, unnormalized: 72 possible variants against 8 requests → every request is a miss, ~1.6 TB/day, an 8x increase in origin egress.

A Vary header multiplies a CDN's cache entries: the edge stores one object per distinct combination of the listed request headers. In this 2026 model of a 1-million-URL catalog with a 24-hour TTL and eight requests per URL per TTL, normalizing Accept-Encoding to three buckets holds edge hit ratio near 62%, while passing raw client values through — commonly 6 to 12 distinct strings per object, with tail measurements finding hundreds — drops it to roughly 25% and triples origin egress.

Which Vary values are safe at the edge

Vary valueDistinct values seen in the wildSafe at the edge?Handling
Accept-Encoding6–12 typical, hundreds in the tail (estimate)Yes, after normalizationCollapse to zstd / br / gzip / identity before the cache key
Accept-Language20–500 raw stringsOnly after mappingMap to the locale set you actually publish; cap variants at that number
Accept (image negotiation)5–20Yes, bucketedThree buckets: avif, webp, legacy
Origin (CORS)One per calling originYes if the allowlist is shortOtherwise return a fixed allow-origin value and drop Vary
User-Agent10,000 to 1,000,000+NoHave the edge set a device-class header with 2–3 values and Vary on that
CookieEffectively unboundedNoStrip analytics cookies at the edge, or mark the route uncacheable and be honest about it
Vary: *n/aNever cacheableRemove it unless you mean "do not store"

The single rule that falls out of this table: only Vary on a header whose value space you control, and if you do not control it, normalize it into a set you do.

vary accept-encoding: normalize to three buckets

Rewrite the request header before the cache lookup, then still emit Vary: Accept-Encoding downstream so browsers and intermediate caches behave correctly. nginx as a caching proxy:

map $http_accept_encoding $norm_ae {
    default   "";
    "~*zstd"  "zstd";
    "~*br"    "br";
    "~*gzip"  "gzip";
}

server {
    location / {
        proxy_set_header Accept-Encoding $norm_ae;
        proxy_cache      edge_zone;
        proxy_cache_key  "$scheme$request_method$host$request_uri$norm_ae";
        proxy_pass       http://origin_upstream;
    }
}

Varnish, same idea in vcl_recv, applied before the built-in lookup:

sub vcl_recv {
    if (req.http.Accept-Encoding ~ "zstd") {
        set req.http.Accept-Encoding = "zstd";
    } elsif (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;
    }

    if (req.url ~ "\.(mp4|m4s|jpg|png|webp|zip|gz)$") {
        unset req.http.Accept-Encoding;
    }
}

The second block matters more than most teams expect. Already-compressed media gains nothing from transfer encoding, and a Vary header on those objects doubles or triples the entry count for zero byte savings — on a video library that is the difference between one variant per segment and three.

Trade-offs and failure modes

Collapsing to fewer buckets costs bytes. Brotli typically ships text 15–20% smaller than gzip; if you normalize everything to gzip to get one variant, you pay that on every text response. Three buckets is usually the right compromise, two if your traffic is overwhelmingly modern browsers.

Serving compressed-on-the-fly at the edge avoids variants entirely but moves CPU to the edge tier and complicates range requests on large objects. It is a reasonable choice for small text assets and a poor one for anything above a few hundred KB.

The dangerous failure is a mismatch between what the edge keys on and what the origin actually varies. If the origin returns Brotli but forgets Vary: Accept-Encoding, a client that only accepts gzip eventually receives a Brotli body and renders binary garbage. Conditional requests make this worse: a weak ETag reused across variants can produce a 304 that revalidates the wrong stored copy.

Purge behavior is the second trap. Some CDNs invalidate every variant behind a URL; others invalidate only the variant matching the purge request's headers, leaving stale siblings alive until TTL expiry. Test this explicitly before you rely on purge for incident response.

The observability gap is that almost every hit-ratio dashboard aggregates by URL, not by variant. Fragmentation is invisible until you log the computed cache key or the normalized header alongside the hit or miss status. Whether header normalization runs before the cache key is computed is worth confirming during vendor evaluation; BlazingCDN exposes this kind of edge cache key and header configuration per customer rather than requiring an origin-side workaround.

Verify this on your own stack

  • Log raw Accept-Encoding and Accept-Language on 24 hours of edge requests, then count distinct values per URL. Anything above 4 for Accept-Encoding means you are not normalizing.
  • Compute fill amplification: origin fills divided by unique URLs requested in one TTL window. Above 1.5 without a deliberate variant strategy, Vary is the first suspect.
  • Grep origin responses for Vary containing User-Agent, Cookie, or an asterisk. Each occurrence is either a hit-ratio bug or an uncacheable route you should be routing differently.
  • Request the same URL twice with different encoding tokens and compare the cache status header on the second request. A miss means the two clients live in separate cache slots.

FAQ: Vary header and cache fragmentation

What does the Vary header do on a CDN?

The Vary header tells a CDN which request headers form the secondary cache key, so the edge stores and serves a separate copy of the object for each distinct combination of those header values. It exists to prevent a cache from serving a Brotli body to a gzip-only client, and it directly determines how many entries one URL occupies.

Is Vary: Accept-Encoding bad for cache hit ratio?

Vary: Accept-Encoding is safe only when the edge normalizes the header first. Raw client values commonly resolve to 6–12 distinct strings per object because of token order and whitespace differences, splitting one URL across that many cache slots. Collapsing to zstd, Brotli, gzip, and identity holds the variant count at four or fewer.

Why should you never use Vary: User-Agent?

User-Agent has effectively unbounded cardinality — tens of thousands to over a million distinct strings in production traffic — so varying on it gives nearly every client its own cache entry and drives hit ratio toward zero. Detect the device at the edge, set a normalized device-class header with two or three values, and vary on that instead.

Run the variant audit this week

Pick your ten highest-traffic URLs and pull 24 hours of edge logs with the raw Accept-Encoding, Accept-Language, and cache status fields. Count distinct header values per URL, multiply them, and compare that product against your average requests-per-URL-per-TTL. If the product is larger, those objects are never reaching a steady-state hit — they are being refilled continuously, and your origin bill reflects it. Then apply three-bucket normalization to one hostname, hold it for a week, and measure the change in origin fills rather than hit ratio. Fills are the number that maps to money.