A CDN that caches everything under one key serves stale or broken content. One that partitions too aggressively destroys its hit ratio. Effective cache partitioning is the engineering discipline of balancing correctness and efficiency at the edge.
Modern origins rarely return a single representation for a given URL. The same /api/products endpoint may return JSON or Protobuf, compressed or uncompressed, English or Japanese, desktop or mobile HTML. If your edge cache stores only one variant per URL, clients will receive the wrong representation. Cache partitioning solves this by creating distinct cache slots for each meaningful variant while keeping the hit ratio as high as possible.
For teams running high-traffic properties, partitioning decisions directly impact origin load, Time to First Byte (TTFB), and bandwidth costs. A misconfigured Vary header can double origin traffic overnight. An overly broad cache key can serve private data to the wrong user. This guide covers the three layers of partitioning every CDN engineer must master.
1. Why Cache Partitioning Matters
At its core, a CDN cache is a key-value store. The cache key is typically derived from the request URL, but the value — the cached response — depends on many other dimensions: request headers, device class, language, authentication state, and even edge location. Without partitioning, the cache suffers from collisions.
Consider an image optimization service. A request for /hero.jpg from a Chrome browser that supports image/webp should return WebP. The same URL from Safari 14 should return JPEG. If the first response is cached without partitioning on Accept, the second client receives an unsupported format and shows a broken image. The reverse is also true: serving JPEG to a WebP-capable client wastes 25-35% bandwidth.
Partitioning also affects performance observability. When you segment correctly, your CDN performance metrics like cache hit ratio become meaningful per segment. A 92% overall hit ratio can hide a 45% hit ratio for your API tier if you are not measuring by partition. Effective segmentation lets you isolate low-performing content types and tune them independently, which is essential for any serious monitoring and observability strategy.
2. The Vary Header: HTTP's Native Partitioning
The Vary response header is defined in RFC 9110 as the standard way for an origin to tell caches which request headers affect content selection. When an origin responds with Vary: Accept-Encoding, Accept-Language, it instructs any downstream cache — browser, CDN, or proxy — to store separate variants keyed by those request headers.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
CDNs respect Vary automatically, but how they implement it varies. Most edge platforms normalize the listed headers (lowercasing, trimming, sorting) and append their values to the internal cache key. A request with Accept-Encoding: gzip and one with Accept-Encoding: br will result in two objects, even though the URL is identical.
When Vary Works Well
Vary is ideal for content negotiation that is already standardized:
- Accept-Encoding: For compression. Most CDNs handle this automatically and may even compress on the fly at the edge. You should almost always include this. See our guide to CDN image optimization for how Accept and Accept-Encoding interact.
- Accept-Language: For localized pages or API messages. Use only if you truly localize at the origin.
- Accept: For API versioning or format negotiation (JSON vs XML).
When Vary Becomes Dangerous
The most common mistake is Vary: User-Agent. User-Agent strings have thousands of variants. Partitioning on the full string effectively creates a cache miss for nearly every request, collapsing your hit ratio to single digits and turning your CDN into an expensive proxy. Never use Vary: User-Agent raw. If you need device-specific variants, normalize at the edge first.
Another pitfall is Vary: *, which tells caches not to use the response for any subsequent request — effectively disabling caching. Some misconfigured origins add this when they set cookies, which is rarely intended.
# Good: Specific, low-cardinality negotiation
HTTP/1.1 200 OK
Content-Type: text/html
Content-Encoding: br
Vary: Accept-Encoding, Accept-Language
Cache-Control: public, max-age=86400
# Bad: High-cardinality, cache-busting
HTTP/1.1 200 OK
Content-Type: text/html
Vary: User-Agent, Cookie, Referer
Cache-Control: public, max-age=86400 # Will have <10% hit ratio
If you must vary on User-Agent, do it via device classification at the edge. Cloudflare, Fastly, and Akamai all offer device detection that maps thousands of UA strings into 3-4 buckets (desktop, mobile, tablet, bot) before the cache lookup. This keeps cardinality low while preserving correctness.
3. Cache Key Design: Beyond the URL
While Vary is origin-driven, cache keys are CDN-driven. Most providers let you explicitly define what constitutes a cache key, overriding or extending Vary. This is where you have the most control — and the most risk.
A default cache key is usually host + path + query string. Customization lets you include or exclude components:
| Key Component | Include When | Risk if Misused | Example |
|---|---|---|---|
| Query String | It changes content (pagination, filters) | Cache fragmentation from tracking params (utm_*) | ?page=2 vs ?utm_source=x |
| Accept / Accept-Language | Origin does content negotiation | Over-partitioning if values are diverse | Accept: image/webp |
| Cookie / Auth | Almost never for public cache | Private data leakage, 0% hit ratio | session_id |
| Device Class | Origin returns different HTML per device | Hit ratio drop if too many buckets | X-Device: mobile |
| Geo / Edge Location | Geo-personalized content (pricing, compliance) | Unnecessary duplication for global content | X-Geo: EU |
Normalization is Non-Negotiable
Before adding a header to the key, normalize it. Sort query parameters alphabetically, lowercase header values, strip irrelevant cookies, and collapse whitespace. Two requests for /search?q=cdn&lang=en and /search?lang=en&q=cdn should hit the same object.
Similarly, strip marketing parameters from the key while preserving them for analytics. Most CDNs offer query string allowlists or denylists:
sub vcl_hash {
# Base key: host + path
set req.hash += req.http.host;
set req.hash += req.url.path;
# Allowlist only meaningful query params
if (querystring.get(req.url, "page")) {
set req.hash += querystring.get(req.url, "page");
}
if (querystring.get(req.url, "lang")) {
set req.hash += querystring.get(req.url, "lang");
}
# utm_* params are ignored for caching
# Normalized device class (set earlier in vcl_recv)
set req.hash += req.http.X-Device-Class; # mobile | desktop | tablet
# Language variant
set req.hash += req.http.Accept-Language;
return (lookup);
}
sub vcl_recv {
# Normalize device: map 1000s of UAs to 3 buckets
if (req.http.User-Agent ~ "Mobile|Android|iPhone") {
set req.http.X-Device-Class = "mobile";
} else {
set req.http.X-Device-Class = "desktop";
}
# Remove Cookie for anonymous content to allow caching
if (req.url.path ~ "^/static/") {
unset req.http.Cookie;
}
}
# Cloudflare Cache Rule: Cache Key -> Custom
# Include: Host, Path, Query (allowlist: page, lang, format)
# Exclude: utm_source, utm_medium, fbclid
# Nginx proxy_cache_key
proxy_cache_key "$scheme$host$uri$arg_page$arg_lang$http_accept_language$http_x_device";
# Note: $args would include all query params — avoid it
A critical security note: never include Cookie or Authorization in the cache key for shared caches unless you are building a private, per-user cache. Doing so will cache authenticated responses and serve them to unauthenticated users. For personalized content, use Cache-Control: private or Vary: Cookie with edge authentication, and consider how your DDoS protection layer interacts with authenticated cache bypasses.
4. Content Segmentation Strategies
Cache keys handle variants of a single URL. Content segmentation handles different caching policies for different types of content. Not all content should be cached the same way, or at all.
Segment your content into tiers and apply distinct partitioning and TTL strategies to each:
Tier 1: Static Immutable Assets
Hashed assets like /assets/app.a3f9c1.js or /images/logo.svg should have the longest TTL (1 year) and the simplest key (URL only). Use content hashing so each deploy creates a new URL — no purging needed. Partitioning on headers is wasteful here; a JPEG is a JPEG regardless of language.
Tier 2: Semi-Static Content
Marketing pages, blog posts, product descriptions. Cache for minutes to hours. Partition on Accept-Encoding and Accept-Language if localized, and optionally on device class if you serve adaptive HTML. Use stale-while-revalidate to hide origin latency. Your DNS resolution path also affects how quickly users reach the right edge for these pages, so keep TTLs aligned.
Tier 3: Dynamic / API Responses
API endpoints, search results, cart data. Cache for seconds or not at all. Partition carefully: include only query params that affect the result set, exclude session tokens. For GraphQL, consider caching at the edge by normalizing the POST body into a cache key — many CDNs now support this via workers.
For APIs, prefer surrogate keys (cache tags) over URL-based purging. Tag responses with Surrogate-Key: product-123 category-shoes and purge by tag when data changes. This avoids the thundering herd problem of purging thousands of URLs.
# Static asset — immutable, 1 year
Cache-Control: public, max-age=31536000, immutable
Surrogate-Key: static assets
# Product page — 5 min at edge, stale for 1 hour
Cache-Control: public, max-age=300, stale-while-revalidate=3600
Surrogate-Key: product-123 product-list
Vary: Accept-Encoding, Accept-Language
# API — 10s, must revalidate, partitioned by query
Cache-Control: public, max-age=10, must-revalidate
Surrogate-Key: api-products
Vary: Accept
Segmentation also enables different shielding strategies. Place static assets on all edges globally, but shield dynamic API traffic through a single regional origin shield to improve coalescing and reduce origin load.
5. Pitfalls, Monitoring & Tuning
Partitioning errors are silent until they cause incidents. Build guardrails:
- Measure per-partition hit ratio. Don't rely on a global average. Break down by content tier, device class, and language. A sudden drop in one partition often signals a new header or query param leaking into the key.
- Enforce a cardinality budget. Limit custom key dimensions to 3-4 low-cardinality values. If a dimension has >20 unique values per hour, it probably shouldn't be in the key.
- Log cache status. Emit X-Cache: HIT/MISS and X-Cache-Key (hashed) to your logs. Correlate misses with the normalized key to spot fragmentation.
- Test with cache poisoning in mind. Verify that a request with a malicious X-Forwarded-Host or extra cookie doesn't create a poisoned variant that is served to others.
- Purge by tag, not by URL. When you partition heavily, a single logical page may have 12 cached variants. Purging by surrogate key invalidates all variants atomically.
Finally, revisit your partitioning quarterly. As you add languages, image formats (AVIF), or A/B test buckets, your key design must evolve. What was correct at 3 variants can become pathological at 30. The goal is not to cache everything — it is to cache the right variants efficiently while keeping the origin authoritative for the rest.
Correct partitioning is a force multiplier: higher hit ratios mean lower origin costs, faster TTFB, and more headroom for your edge security controls to operate without adding latency.
Frequently Asked Questions
What is the difference between Vary and a custom cache key?
Vary is an origin-declared instruction that tells any cache which request headers create different variants. A custom cache key is a CDN-specific configuration that explicitly defines the key composition, often normalizing or mapping headers before they enter the key. Use Vary for standards-based negotiation (Accept-Encoding) and custom keys when you need normalization, allowlisting, or device classification that Vary alone cannot provide.
How does over-partitioning affect cache hit ratio?
Each additional key dimension multiplies the number of cache objects. Adding full User-Agent (thousands of values) or an unbounded query string can fragment a single URL into thousands of objects, dropping hit ratio from 90%+ to under 10%. This increases origin load, latency, and egress costs. Keep dimensions to 2-4 low-cardinality values (e.g., 3 device classes, 5 languages) and strip irrelevant parameters.
Should you include User-Agent in cache keys?
Never include the raw User-Agent string. Its high cardinality destroys cache efficiency. If you need device-specific rendering, classify the User-Agent at the edge into a small set of buckets (mobile, desktop, tablet) and include only that normalized bucket in the key. Most CDN providers offer built-in device detection for this purpose.
How do CDNs handle Vary: Accept-Encoding automatically?
Most modern CDNs handle Accept-Encoding outside of normal Vary logic. They store one uncompressed origin response and compress it at the edge to gzip, Brotli, or Zstandard based on the client's Accept-Encoding, or they store 2-3 compressed variants transparently. You typically do not need to configure this manually, but you should ensure your origin does not send Vary: * and that you forward Accept-Encoding correctly to the origin if needed.