Introduction

CDN cache key misconfiguration errors occur when the CDN generates incorrect or inconsistent cache keys for resources, resulting in cache misses when hits should occur, serving wrong content to users, or cache fragmentation that reduces hit ratios. The cache key uniquely identifies a cached object and typically includes the URL path, query string parameters, host header, and other request attributes based on configuration. When cache keys are misconfigured, identical requests may generate different cache keys (reducing hit ratio), or different requests may incorrectly share the same cached response (serving wrong content). Common causes include query string parameters not normalized (order matters), session IDs or timestamps in URLs cached as separate objects, missing Vary header handling causing wrong content served, cookies included in cache key when they shouldn't be, URL case sensitivity issues, www vs non-www creating duplicate cache entries, HTTP vs HTTPS creating separate cache entries, and mobile/desktop variants cached incorrectly. The fix requires understanding cache key generation, proper query string handling, Vary header processing, and CDN-specific cache key configuration. This guide provides production-proven troubleshooting for cache key issues across Cloudflare, AWS CloudFront, Fastly, Akamai, and generic CDN implementations.

Symptoms

  • Low cache hit ratio despite high traffic (<50%)
  • Same resource cached multiple times with different keys
  • Users receiving wrong/stale content for their request
  • Cache key includes session IDs or user-specific data
  • Query string order affects cache (a=1&b=2 vs b=2&a=1)
  • Mobile users receiving desktop content or vice versa
  • Compressed vs uncompressed content mixed up
  • Different languages serving wrong localized content
  • Origin traffic higher than expected (cache misses)
  • Cache statistics show high "cardinality" (too many unique keys)

Common Causes

  • Full query string included in cache key without normalization
  • Session IDs, timestamps, or tracking params creating unique keys
  • Vary header not respected (Accept-Encoding, Accept-Language)
  • Cookies included in cache key unnecessarily
  • URL path case differences (/Page vs /page)
  • WWW and non-WWW hosts cached separately
  • HTTP and HTTPS cached separately without normalization
  • Mobile/desktop User-Agent creating separate cache entries
  • A/B test variant IDs in URL cached separately
  • CDN not configured to ignore cache-busting query params

Step-by-Step Fix

### 1. Diagnose cache key configuration

Check current cache key:

```bash # Cloudflare: Check cache key in response headers curl -I https://example.com/resource # Look for: CF-Cache-Status: HIT or MISS

# Cloudflare: Use Worker to inspect cache key addEventListener('fetch', event => { event.respondWith( new Response(JSON.stringify({ url: event.request.url, host: event.request.headers.get('Host'), cacheKey: event.request.url })) ) })

# CloudFront: Check cache policy aws cloudfront get-cache-policy --id POLICY_ID

# Fastly: Inspect cache key via VCL # Add to VCL for debugging: # set req.http.X-Cache-Key = req.url; # Fastly debug panel shows cache key

# Akamai: Use EdgeGrid to inspect # Or check Property Manager cache key settings ```

Analyze cache hit/miss patterns:

```bash # Test same resource with different query orders curl -I "https://cdn.example.com/resource?a=1&b=2" curl -I "https://cdn.example.com/resource?b=2&a=1" # Both should return CF-Cache-Status: HIT after first request

# Test with tracking parameters curl -I "https://cdn.example.com/resource?utm_source=test" curl -I "https://cdn.example.com/resource?utm_source=other" # Should be same cache entry if tracking params ignored

# Test case sensitivity curl -I "https://cdn.example.com/Page" curl -I "https://cdn.example.com/page" # Should be same cache entry if normalized ```

### 2. Configure query string handling

Cloudflare cache key configuration:

```bash # Cloudflare Dashboard > Caching > Cache Rules

# Include specific query parameters only # Dashboard > Caching > Configuration > Query String Sort # Enable: Sort Query Parameters

# Or use Page Rules to strip parameters # Dashboard > Page Rules > Create Rule # URL: example.com/* # Setting: Cache Level = Cache Everything # Setting: Cache Key Query Parameters = Exclude # Parameters to exclude: utm_*, fbclid, gclid, _ga

# Cloudflare Workers for advanced cache key logic addEventListener('fetch', event => { const url = new URL(event.request.url)

// Remove tracking parameters from cache key const paramsToRemove = ['utm_source', 'utm_medium', 'utm_campaign', 'fbclid', 'gclid', '_ga', '_gid'] paramsToRemove.forEach(param => url.searchParams.delete(param))

// Sort remaining parameters for consistent key const sortedParams = new URLSearchParams([...url.searchParams.entries()] .sort((a, b) => a[0].localeCompare(b[0]))) url.search = sortedParams.toString()

const newRequest = new Request(url.toString(), event.request) event.respondWith(fetch(newRequest)) }) ```

AWS CloudFront cache policy:

```bash # Create cache policy with specific query parameters aws cloudfront create-cache-policy \ --cache-policy-config ' { "Name": "MyCachePolicy", "MinTTL": 60, "DefaultTTL": 3600, "MaxTTL": 86400, "ParametersInCacheKeyAndForwardedToOrigin": { "CookiesConfig": { "CookieBehavior": "none" # Don't include cookies in cache key }, "HeadersConfig": { "HeaderBehavior": "whitelist", "Headers": { "Quantity": 2, "Items": ["Accept", "Accept-Language"] # Only these headers } }, "QueryStringsConfig": { "QueryStringBehavior": "whitelist", "QueryStrings": { "Quantity": 2, "Items": ["version", "locale"] # Only cache on these } }, "EnableAcceptEncodingGzip": true, "EnableAcceptEncodingBrotli": true } }'

# Or exclude specific query parameters # Use "allowList" to include only specific params # Use "allExcept" to exclude specific params (not directly supported, # use CloudFront Functions to modify cache key) ```

CloudFront Functions for cache key normalization:

```javascript // CloudFront Function to normalize cache key function handler(event) { var request = event.request; var uri = request.uri; var querystring = request.querystring;

if (querystring) { // Parse query string var params = querystring.split('&'); var filteredParams = [];

for (var i = 0; i < params.length; i++) { var param = params[i].split('='); var key = param[0];

// Exclude tracking parameters if (!key.startsWith('utm_') && !key.startsWith('fbclid') && !key.startsWith('gclid') && !key.startsWith('_ga')) { filteredParams.push(params[i]); } }

// Sort parameters for consistent cache key filteredParams.sort(); request.querystring = filteredParams.join('&'); }

// Normalize URI case (optional) // request.uri = uri.toLowerCase();

return request; } ```

### 3. Fix Vary header issues

Handle Accept-Encoding (compression):

```bash # Ensure origin sends proper Vary header # Origin response should include: # Vary: Accept-Encoding

# Cloudflare: Automatically handles Accept-Encoding # Just ensure origin sends Vary: Accept-Encoding

# CloudFront: Enable compression in cache policy aws cloudfront update-cache-policy \ --id POLICY_ID \ --cache-policy-config '{ "ParametersInCacheKeyAndForwardedToOrigin": { "EnableAcceptEncodingGzip": true, "EnableAcceptEncodingBrotli": true } }'

# NGINX origin configuration gzip on; gzip_types text/plain application/json text/css application/javascript; gzip_vary on; # Adds Vary: Accept-Encoding header ```

Handle Accept-Language (localization):

```bash # If serving localized content, cache key must include language # Origin response: Vary: Accept-Language

# Cloudflare: Create cache rule with language # Include Accept-Language header in cache key

# CloudFront: Whitelist Accept-Language header aws cloudfront create-cache-policy \ --cache-policy-config '{ "ParametersInCacheKeyAndForwardedToOrigin": { "HeadersConfig": { "HeaderBehavior": "whitelist", "Headers": { "Quantity": 1, "Items": ["Accept-Language"] } } } }'

# Fastly VCL configuration if (req.http.Accept-Language) { set req.http.X-Language = regsub(req.http.Accept-Language, "^([a-z]+).*", "\1"); } # Then include X-Language in cache key ```

Handle User-Agent (mobile/desktop):

```bash # Only include in cache key if serving different content # Don't cache on full User-Agent (too much variation)

# Cloudflare Workers: Normalize User-Agent addEventListener('fetch', event => { const ua = event.request.headers.get('User-Agent') || '' const isMobile = /Mobile|Android|iPhone/i.test(ua)

// Create normalized cache key const url = new URL(event.request.url) url.searchParams.set('device_type', isMobile ? 'mobile' : 'desktop')

const newRequest = new Request(url.toString(), event.request) event.respondWith(fetch(newRequest)) }) ```

### 4. Fix cookie handling

Strip cookies from cache key:

```bash # Most static content shouldn't cache on cookies # Cloudflare: Cookies automatically excluded from cache key # But ensure origin doesn't set cookies on static assets

# CloudFront: Configure cookie behavior aws cloudfront create-cache-policy \ --cache-policy-config '{ "ParametersInCacheKeyAndForwardedToOrigin": { "CookiesConfig": { "CookieBehavior": "none" # Exclude all cookies } } }'

# For dynamic content that needs cookies: aws cloudfront create-cache-policy \ --cache-policy-config '{ "ParametersInCacheKeyAndForwardedToOrigin": { "CookiesConfig": { "CookieBehavior": "whitelist", "Cookies": { "Quantity": 2, "Items": ["session_id", "user_pref"] } } } }' ```

Fastly VCL cookie stripping:

```vcl # Remove cookies from request before cache lookup sub vcl_recv { # Remove all cookies unset req.http.Cookie;

# Or remove specific cookies if (req.http.Cookie) { set req.http.Cookie = regsuball(req.http.Cookie, "^__utm=[^;]+;?\s*", ""); set req.http.Cookie = regsuball(req.http.Cookie, "^_ga=[^;]+;?\s*", ""); set req.http.Cookie = regsuball(req.http.Cookie, "^_gid=[^;]+;?\s*", "");

# Clean up trailing semicolon set req.http.Cookie = regsuball(req.http.Cookie, ";+$", "");

# Remove Cookie header if empty if (req.http.Cookie == "") { unset req.http.Cookie; } } } ```

### 5. Normalize URLs

Case normalization:

```bash # Cloudflare: Dashboard > Caching > Configuration # Enable: Normalize Cache Keys

# CloudFront Functions function handler(event) { var request = event.request; // Normalize to lowercase request.uri = request.uri.toLowerCase(); return request; }

# NGINX origin location / { # Try lowercase version first try_files $uri $uri/ /index.html; } ```

WWW/non-WWW normalization:

```bash # Ensure all traffic goes through single canonical host # Cloudflare: Page Rule for redirect # URL: http://example.com/* # Setting: Always Use HTTPS # Setting: WWW to Subdomain

# CloudFront: Alternate domain names (CNAMEs) # Both www.example.com and example.com point to same distribution # Cache key uses Host header, so normalize:

# CloudFront Functions function handler(event) { var request = event.request; var headers = request.headers;

// Normalize to non-WWW if (headers.host.value.startsWith('www.')) { headers.host.value = headers.host.value.substring(4); }

return request; } ```

### 6. Debug cache key issues

Enable cache key logging:

```bash # Cloudflare: Use Workers to log cache keys addEventListener('fetch', event => { const url = new URL(event.request.url)

// Log cache key info console.log(JSON.stringify({ url: url.toString(), host: event.request.headers.get('Host'), userAgent: event.request.headers.get('User-Agent'), acceptLanguage: event.request.headers.get('Accept-Language'), cookie: event.request.headers.get('Cookie') }))

event.respondWith(fetch(event.request)) })

# Fastly: Use logging endpoint # Configure logging to include cache key # Dashboard > Logging > Add endpoint # Format: %{req.url} %{req.http.host} %{hash}

# CloudFront: Enable real-time logging # Create Kinesis Data Firehose # Configure distribution to send logs ```

Analyze cache cardinality:

```bash # Cloudflare: Analytics > Caching # Check "Cache Hit Ratio" and "Total Requests" # High unique URLs = high cardinality

# Look for patterns in miss URLs: # - Session IDs: /product/123?session=abc123 # - Timestamps: /api/data?t=1234567890 # - Tracking: /page?utm_source=xxx&utm_medium=yyy

# Fastly: Real-time analytics # Dashboard > Analytics > Requests # Look for high "Unique Objects Cached"

# Akamai: Control Center > Reports # Cache efficiency reports ```

Prevention

  • Audit cache key configuration during CDN setup
  • Document which query params affect content vs tracking
  • Use cache key normalization for all public content
  • Exclude session IDs and user-specific data from cache keys
  • Properly configure Vary headers for content variants
  • Monitor cache hit ratio and alert on significant drops
  • Regular cache key audits using real traffic data
  • Test cache behavior with different URL patterns
  • Use cache tags for efficient invalidation
  • Document cache key rules for team reference
  • **CDN cache invalidation purge error**: Cache not updating correctly
  • **CDN origin connection timeout**: Origin too slow to respond
  • **CDN 502 Bad Gateway origin failed**: Origin returning errors
  • **CDN 503 Origin Unreachable**: Origin not accessible