[{"data":1,"prerenderedAt":1076},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate":1068},{"id":4,"title":5,"body":6,"description":1048,"extension":1049,"meta":1050,"navigation":1062,"path":1063,"seo":1064,"stem":1066,"__hash__":1067},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate\u002Findex.md","Vary Header Pitfalls That Destroy CDN Hit Rate",{"type":7,"value":8,"toc":1040},"minimark",[9,14,38,44,49,96,261,265,279,288,300,310,314,319,439,442,457,530,533,539,816,819,834,838,841,847,911,917,921,936,945,957,966,983,1001,1005,1025,1030,1033,1036],[10,11,13],"h1",{"id":12},"how-a-single-vary-header-destroys-your-cdn-hit-rate","How a Single Vary Header Destroys Your CDN Hit Rate",[15,16,17,18,23,24,28,29,33,34,37],"p",{},"This walkthrough extends ",[19,20,22],"a",{"href":21},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002F","CDN Edge Caching Configuration"," within ",[19,25,27],{"href":26},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",". Your ",[30,31,32],"code",{},"Cache-Control"," is correct, your TTLs are generous, and your hit ratio is 41%. The cause is usually one line: a ",[30,35,36],{},"Vary"," header naming something with near-infinite cardinality, which instructs every cache to store a separate copy of the response per distinct value of that header.",[15,39,40,43],{},[30,41,42],{},"Vary: User-Agent"," alone can turn one cacheable object into tens of thousands of near-identical ones, none of which is ever warm.",[45,46,48],"h2",{"id":47},"rapid-diagnosis-confirm-fragmentation","Rapid Diagnosis: Confirm Fragmentation",[50,51,52,71,80,90],"ul",{},[53,54,55,62,63,66,67,70],"li",{},[56,57,58,59,61],"strong",{},"Read the response's ",[30,60,36],{}," header."," Anything beyond ",[30,64,65],{},"Accept-Encoding"," — and occasionally ",[30,68,69],{},"Accept"," — deserves justification.",[53,72,73,76,77,79],{},[56,74,75],{},"Check the hit ratio by path."," Fragmentation shows as a low ratio on a path whose content is identical for everyone; static assets with a bad ",[30,78,36],{}," are the clearest case.",[53,81,82,89],{},[56,83,84,85,88],{},"Look for ",[30,86,87],{},"Vary: Cookie"," on HTML."," Any cookie value change — including analytics IDs that differ per visitor — creates a new cache entry. With per-visitor cookies, the hit ratio for that path is effectively zero.",[53,91,92,95],{},[56,93,94],{},"Compare edge and origin request volume."," If origin traffic tracks visitor count rather than content count, the cache is not doing anything.",[97,98,105,106,113,114,113,118,113,122,113,131,113,138,113,146,113,153,113,157,113,164,113,169,113,179,113,185,113,191,113,194,113,197,113,200,113,203,113,207,113,211,113,216,113,220,113,224,113,228,113,234,113,237,113,241,113,244,113,248,113,251,113,256,113],"svg",{"xmlns":99,"viewBox":100,"width":101,"role":102,"ariaLabel":103,"style":104},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 252","100%","img","Diagram showing one URL fanning out into thousands of cache entries when varying on User-Agent and Cookie, versus a single entry after normalising the cache key","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)","\n  ",[107,108],"rect",{"className":109,"x":111,"y":111,"width":101,"height":101,"fill":112},[110],"svg-canvas","0","#ffffff"," ",[115,116,117],"title",{},"Cache-key fan-out from a wide Vary",[119,120,121],"desc",{},"Varying on User-Agent and Cookie multiplies one URL into thousands of stored objects with near-zero hit probability; normalising to a small set of device classes collapses it back to a handful.",[107,123],{"x":124,"y":124,"width":125,"height":126,"rx":127,"fill":128,"stroke":129,"style":130},"1","758","250","10","none","currentColor","stroke-opacity:0.18",[132,133,137],"text",{"x":134,"y":135,"fill":129,"style":136},"24","32","font-size:16px;font-weight:700","One URL, how many stored objects?",[107,139],{"x":140,"y":141,"width":142,"height":140,"rx":143,"fill":144,"stroke":144,"style":145},"40","70","130","6","#0466c8","fill-opacity:0.18;stroke-opacity:0.55",[132,147,152],{"x":148,"y":149,"fill":129,"style":150,"textAnchor":151},"105","95","font-size:11.5px;font-weight:700;","middle","\u002Fproducts",[132,154,156],{"x":148,"y":142,"fill":129,"style":155,"textAnchor":151},"font-size:11px;","one response body",[158,159],"line",{"x1":160,"y1":161,"x2":162,"y2":161,"stroke":129,"style":163},"170","90","206","stroke-opacity:0.4",[165,166],"polygon",{"points":167,"fill":129,"style":168},"214,90 204,85 204,95","fill-opacity:0.55",[107,170],{"x":171,"y":172,"width":173,"height":174,"rx":175,"fill":176,"stroke":177,"style":178},"214","56","220","34","5","#ffc300","#b8860b","fill-opacity:0.28",[132,180,184],{"x":181,"y":182,"fill":129,"style":183,"textAnchor":151},"324","78","font-size:11px;font-weight:700;","Vary: User-Agent, Cookie",[107,186],{"x":171,"y":187,"width":174,"height":188,"rx":189,"fill":129,"style":190},"98","26","3","fill-opacity:0.16",[107,192],{"x":193,"y":187,"width":174,"height":188,"rx":189,"fill":129,"style":190},"252",[107,195],{"x":196,"y":187,"width":174,"height":188,"rx":189,"fill":129,"style":190},"290",[107,198],{"x":199,"y":187,"width":174,"height":188,"rx":189,"fill":129,"style":190},"328",[107,201],{"x":202,"y":187,"width":174,"height":188,"rx":189,"fill":129,"style":190},"366",[107,204],{"x":205,"y":187,"width":206,"height":188,"rx":189,"fill":129,"style":190},"404","30",[132,208,210],{"x":181,"y":209,"fill":177,"style":150,"textAnchor":151},"146","~40,000 entries · 41% hit",[158,212],{"x1":213,"y1":214,"x2":213,"y2":173,"stroke":129,"style":215},"460","50","stroke-opacity:0.2",[107,217],{"x":218,"y":172,"width":219,"height":174,"rx":175,"fill":144,"stroke":144,"style":145},"490","230",[132,221,223],{"x":222,"y":182,"fill":129,"style":183,"textAnchor":151},"605","normalised: 3 device classes",[107,225],{"x":226,"y":187,"width":172,"height":188,"rx":189,"fill":144,"style":227},"512","fill-opacity:0.35",[132,229,233],{"x":230,"y":231,"fill":129,"style":232,"textAnchor":151},"540","116","font-size:10px;","mobile",[107,235],{"x":236,"y":187,"width":172,"height":188,"rx":189,"fill":144,"style":227},"578",[132,238,240],{"x":239,"y":231,"fill":129,"style":232,"textAnchor":151},"606","tablet",[107,242],{"x":243,"y":187,"width":172,"height":188,"rx":189,"fill":144,"style":227},"644",[132,245,247],{"x":246,"y":231,"fill":129,"style":232,"textAnchor":151},"672","desktop",[132,249,250],{"x":222,"y":209,"fill":129,"style":150,"textAnchor":151},"3 entries · 96% hit",[132,252,255],{"x":134,"y":253,"fill":129,"style":254},"196","font-size:11.5px;fill-opacity:0.85","Cache capacity is finite: fragmented objects evict each other before they are ever reused.",[132,257,260],{"x":134,"y":258,"fill":129,"style":259},"222","font-size:11px;font-style:italic;fill-opacity:0.7","Hit ratios shown are illustrative of the pattern, not a benchmark — measure your own per path.",[45,262,264],{"id":263},"root-cause-analysis","Root Cause Analysis",[15,266,267,273,274,278],{},[56,268,269,270,272],{},"1. ",[30,271,42],{}," on anything."," User agent strings are effectively unique per browser build. Even where content genuinely differs by device, the correct key is a device ",[275,276,277],"em",{},"class"," of two or three values, not the raw string.",[15,280,281,287],{},[56,282,283,284,286],{},"2. ",[30,285,87],{}," on shared responses."," Cookies carry per-visitor identifiers, so varying on the whole header guarantees a unique key per visitor. If a response really depends on one cookie, key on that cookie's value alone — most CDNs support this directly.",[15,289,290,296,297,299],{},[56,291,292,293,295],{},"3. Framework defaults that add ",[30,294,36],{}," silently."," Compression middleware adds ",[30,298,65],{}," (correct and harmless), but content negotiation, locale detection or session middleware can add much wider values without anyone deciding to.",[15,301,302,309],{},[56,303,304,305,308],{},"4. ",[30,306,307],{},"Vary: *","."," This means \"never reuse this response for any other request\" — a complete cache disable. It occasionally appears from a security middleware and is always worth challenging.",[45,311,313],{"id":312},"step-by-step-resolution","Step-by-Step Resolution",[15,315,316],{},[56,317,318],{},"1. Audit what you are actually sending.",[320,321,326],"pre",{"className":322,"code":323,"language":324,"meta":325,"style":325},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# Every distinct Vary value across the routes that matter.\nfor p in \u002F \u002Fproducts \u002Fproducts\u002Fwidget \u002Fassets\u002Fapp.css; do\n  printf '%-24s ' \"$p\"\n  curl -sI \"https:\u002F\u002Fexample.com$p\" | awk 'tolower($1)==\"vary:\"{ $1=\"\"; print }'\ndone\n# trade-off: this reads the edge's response. Check the ORIGIN too — a CDN can\n# strip or rewrite Vary, so a clean edge response may hide a wide origin value\n# that fragments an intermediate cache.\n","bash","",[30,327,328,336,369,388,415,421,427,433],{"__ignoreMap":325},[329,330,332],"span",{"class":158,"line":331},1,[329,333,335],{"class":334},"sjfSM","# Every distinct Vary value across the routes that matter.\n",[329,337,339,343,347,350,354,357,360,363,366],{"class":158,"line":338},2,[329,340,342],{"class":341},"sPARh","for",[329,344,346],{"class":345},"saISM"," p ",[329,348,349],{"class":341},"in",[329,351,353],{"class":352},"sZ8jY"," \u002F",[329,355,356],{"class":352}," \u002Fproducts",[329,358,359],{"class":352}," \u002Fproducts\u002Fwidget",[329,361,362],{"class":352}," \u002Fassets\u002Fapp.css",[329,364,365],{"class":345},"; ",[329,367,368],{"class":341},"do\n",[329,370,372,376,379,382,385],{"class":158,"line":371},3,[329,373,375],{"class":374},"sPXB4","  printf",[329,377,378],{"class":352}," '%-24s '",[329,380,381],{"class":352}," \"",[329,383,384],{"class":345},"$p",[329,386,387],{"class":352},"\"\n",[329,389,391,395,398,401,403,406,409,412],{"class":158,"line":390},4,[329,392,394],{"class":393},"sQw3B","  curl",[329,396,397],{"class":374}," -sI",[329,399,400],{"class":352}," \"https:\u002F\u002Fexample.com",[329,402,384],{"class":345},[329,404,405],{"class":352},"\"",[329,407,408],{"class":341}," |",[329,410,411],{"class":393}," awk",[329,413,414],{"class":352}," 'tolower($1)==\"vary:\"{ $1=\"\"; print }'\n",[329,416,418],{"class":158,"line":417},5,[329,419,420],{"class":341},"done\n",[329,422,424],{"class":158,"line":423},6,[329,425,426],{"class":334},"# trade-off: this reads the edge's response. Check the ORIGIN too — a CDN can\n",[329,428,430],{"class":158,"line":429},7,[329,431,432],{"class":334},"# strip or rewrite Vary, so a clean edge response may hide a wide origin value\n",[329,434,436],{"class":158,"line":435},8,[329,437,438],{"class":334},"# that fragments an intermediate cache.\n",[15,440,441],{},"Expected outcome: a short list of offending routes, usually one or two middleware sources.",[15,443,444,450,451,453,454,456],{},[56,445,446,447,449],{},"2. Reduce ",[30,448,36],{}," to what genuinely changes the bytes."," For most responses that is ",[30,452,65],{}," alone, plus ",[30,455,69],{}," where you negotiate image formats.",[320,458,462],{"className":459,"code":460,"language":461,"meta":325,"style":325},"language-nginx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# Static assets: identical for everyone, regardless of who asks.\nlocation \u002Fassets\u002F {\n  add_header Cache-Control \"public, max-age=31536000, immutable\" always;\n  add_header Vary \"Accept-Encoding\" always;      # nothing else varies the bytes\n}\n# trade-off: dropping Vary values that DO change the response serves the wrong\n# body to someone. Confirm per route that the response is genuinely identical\n# before narrowing — a wrong Vary is a correctness bug, not just a slow cache.\n","nginx",[30,463,464,469,480,494,510,515,520,525],{"__ignoreMap":325},[329,465,466],{"class":158,"line":331},[329,467,468],{"class":334},"# Static assets: identical for everyone, regardless of who asks.\n",[329,470,471,474,477],{"class":158,"line":338},[329,472,473],{"class":341},"location",[329,475,476],{"class":393}," \u002Fassets\u002F ",[329,478,479],{"class":345},"{\n",[329,481,482,485,488,491],{"class":158,"line":371},[329,483,484],{"class":341},"  add_header ",[329,486,487],{"class":345},"Cache-Control ",[329,489,490],{"class":352},"\"public, max-age=31536000, immutable\"",[329,492,493],{"class":345}," always;\n",[329,495,496,498,501,504,507],{"class":158,"line":390},[329,497,484],{"class":341},[329,499,500],{"class":345},"Vary ",[329,502,503],{"class":352},"\"Accept-Encoding\"",[329,505,506],{"class":345}," always;      ",[329,508,509],{"class":334},"# nothing else varies the bytes\n",[329,511,512],{"class":158,"line":417},[329,513,514],{"class":345},"}\n",[329,516,517],{"class":158,"line":423},[329,518,519],{"class":334},"# trade-off: dropping Vary values that DO change the response serves the wrong\n",[329,521,522],{"class":158,"line":429},[329,523,524],{"class":334},"# body to someone. Confirm per route that the response is genuinely identical\n",[329,526,527],{"class":158,"line":435},[329,528,529],{"class":334},"# before narrowing — a wrong Vary is a correctness bug, not just a slow cache.\n",[15,531,532],{},"Expected outcome: static assets return to a near-100% hit ratio immediately.",[15,534,535,538],{},[56,536,537],{},"3. Replace wide headers with a normalised cache key."," Where the response really does differ, derive a low-cardinality value at the edge and key on that.",[320,540,544],{"className":541,"code":542,"language":543,"meta":325,"style":325},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Edge worker: classify the device, then vary on the derived class only.\nexport default {\n  async fetch(request) {\n    const ua = request.headers.get('user-agent') || '';\n    const cls = \u002FMobi|Android\u002Fi.test(ua) ? 'mobile'\n              : \u002FTablet|iPad\u002Fi.test(ua) ? 'tablet' : 'desktop';\n    const keyed = new Request(request);\n    keyed.headers.set('x-device-class', cls);     \u002F\u002F cache key includes this\n    const res = await fetch(keyed);\n    const out = new Response(res.body, res);\n    out.headers.set('Vary', 'Accept-Encoding, x-device-class');\n    return out;\n  }\n};\n\u002F\u002F trade-off: a derived class is a lossy summary. A device that straddles the\n\u002F\u002F regex gets the wrong variant — acceptable for layout tweaks, not for\n\u002F\u002F anything where the wrong response is a bug.\n","javascript",[30,545,546,551,562,576,611,646,680,698,717,735,753,775,784,790,798,804,810],{"__ignoreMap":325},[329,547,548],{"class":158,"line":331},[329,549,550],{"class":334},"\u002F\u002F Edge worker: classify the device, then vary on the derived class only.\n",[329,552,553,556,559],{"class":158,"line":338},[329,554,555],{"class":341},"export",[329,557,558],{"class":341}," default",[329,560,561],{"class":393}," {\n",[329,563,564,567,571,574],{"class":158,"line":371},[329,565,566],{"class":341},"  async",[329,568,570],{"class":569},"smZ65"," fetch",[329,572,573],{"class":393},"(request) ",[329,575,479],{"class":345},[329,577,578,581,584,587,590,593,596,599,602,605,608],{"class":158,"line":390},[329,579,580],{"class":341},"    const",[329,582,583],{"class":374}," ua",[329,585,586],{"class":341}," =",[329,588,589],{"class":345}," request.headers.",[329,591,592],{"class":569},"get",[329,594,595],{"class":345},"(",[329,597,598],{"class":352},"'user-agent'",[329,600,601],{"class":345},") ",[329,603,604],{"class":341},"||",[329,606,607],{"class":352}," ''",[329,609,610],{"class":345},";\n",[329,612,613,615,618,620,623,626,629,632,634,637,640,643],{"class":158,"line":417},[329,614,580],{"class":341},[329,616,617],{"class":374}," cls",[329,619,586],{"class":341},[329,621,622],{"class":352}," \u002FMobi",[329,624,625],{"class":341},"|",[329,627,628],{"class":352},"Android\u002F",[329,630,631],{"class":341},"i",[329,633,308],{"class":345},[329,635,636],{"class":569},"test",[329,638,639],{"class":345},"(ua) ",[329,641,642],{"class":341},"?",[329,644,645],{"class":352}," 'mobile'\n",[329,647,648,651,654,656,659,661,663,665,667,669,672,675,678],{"class":158,"line":423},[329,649,650],{"class":341},"              :",[329,652,653],{"class":352}," \u002FTablet",[329,655,625],{"class":341},[329,657,658],{"class":352},"iPad\u002F",[329,660,631],{"class":341},[329,662,308],{"class":345},[329,664,636],{"class":569},[329,666,639],{"class":345},[329,668,642],{"class":341},[329,670,671],{"class":352}," 'tablet'",[329,673,674],{"class":341}," :",[329,676,677],{"class":352}," 'desktop'",[329,679,610],{"class":345},[329,681,682,684,687,689,692,695],{"class":158,"line":429},[329,683,580],{"class":341},[329,685,686],{"class":374}," keyed",[329,688,586],{"class":341},[329,690,691],{"class":341}," new",[329,693,694],{"class":569}," Request",[329,696,697],{"class":345},"(request);\n",[329,699,700,703,706,708,711,714],{"class":158,"line":435},[329,701,702],{"class":345},"    keyed.headers.",[329,704,705],{"class":569},"set",[329,707,595],{"class":345},[329,709,710],{"class":352},"'x-device-class'",[329,712,713],{"class":345},", cls);     ",[329,715,716],{"class":334},"\u002F\u002F cache key includes this\n",[329,718,720,722,725,727,730,732],{"class":158,"line":719},9,[329,721,580],{"class":341},[329,723,724],{"class":374}," res",[329,726,586],{"class":341},[329,728,729],{"class":341}," await",[329,731,570],{"class":569},[329,733,734],{"class":345},"(keyed);\n",[329,736,738,740,743,745,747,750],{"class":158,"line":737},10,[329,739,580],{"class":341},[329,741,742],{"class":374}," out",[329,744,586],{"class":341},[329,746,691],{"class":341},[329,748,749],{"class":569}," Response",[329,751,752],{"class":345},"(res.body, res);\n",[329,754,756,759,761,763,766,769,772],{"class":158,"line":755},11,[329,757,758],{"class":345},"    out.headers.",[329,760,705],{"class":569},[329,762,595],{"class":345},[329,764,765],{"class":352},"'Vary'",[329,767,768],{"class":345},", ",[329,770,771],{"class":352},"'Accept-Encoding, x-device-class'",[329,773,774],{"class":345},");\n",[329,776,778,781],{"class":158,"line":777},12,[329,779,780],{"class":341},"    return",[329,782,783],{"class":345}," out;\n",[329,785,787],{"class":158,"line":786},13,[329,788,789],{"class":345},"  }\n",[329,791,793,796],{"class":158,"line":792},14,[329,794,795],{"class":393},"}",[329,797,610],{"class":345},[329,799,801],{"class":158,"line":800},15,[329,802,803],{"class":334},"\u002F\u002F trade-off: a derived class is a lossy summary. A device that straddles the\n",[329,805,807],{"class":158,"line":806},16,[329,808,809],{"class":334},"\u002F\u002F regex gets the wrong variant — acceptable for layout tweaks, not for\n",[329,811,813],{"class":158,"line":812},17,[329,814,815],{"class":334},"\u002F\u002F anything where the wrong response is a bug.\n",[15,817,818],{},"Expected outcome: cardinality falls from tens of thousands to single digits, and the hit ratio recovers to its structural maximum.",[15,820,821,824,825,828,829,833],{},[56,822,823],{},"4. Split personalised fragments out of cacheable HTML."," The strongest fix is architectural: cache the shared shell aggressively and fetch the per-user parts separately, so nothing needs to vary on ",[30,826,827],{},"Cookie"," at all. That approach pairs naturally with ",[19,830,832],{"href":831},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002F","stale-while-revalidate"," on the shell.",[45,835,837],{"id":836},"verification","Verification",[15,839,840],{},"Watch the hit ratio per path for 24 hours after the change — it should rise quickly for static assets and more slowly for HTML as the smaller key space warms. Track origin request volume at the same time: a real fix reduces it proportionally, and if origin traffic is unchanged the cache key is still fragmented somewhere upstream.",[15,842,843,844,846],{},"Then check correctness explicitly, because narrowing ",[30,845,36],{}," is the one cache change that can serve a wrong response:",[320,848,850],{"className":322,"code":849,"language":324,"meta":325,"style":325},"# The same URL with different request shapes should return the right variant.\ncurl -s -H 'User-Agent: Mozilla\u002F5.0 (iPhone…)' https:\u002F\u002Fexample.com\u002Fproducts | md5sum\ncurl -s -H 'User-Agent: Mozilla\u002F5.0 (Windows…)' https:\u002F\u002Fexample.com\u002Fproducts | md5sum\n# Identical hashes mean the variants were never really different — you can drop\n# the device split entirely. Different hashes confirm the normalised key is\n# doing real work.\n",[30,851,852,857,879,896,901,906],{"__ignoreMap":325},[329,853,854],{"class":158,"line":331},[329,855,856],{"class":334},"# The same URL with different request shapes should return the right variant.\n",[329,858,859,862,865,868,871,874,876],{"class":158,"line":338},[329,860,861],{"class":393},"curl",[329,863,864],{"class":374}," -s",[329,866,867],{"class":374}," -H",[329,869,870],{"class":352}," 'User-Agent: Mozilla\u002F5.0 (iPhone…)'",[329,872,873],{"class":352}," https:\u002F\u002Fexample.com\u002Fproducts",[329,875,408],{"class":341},[329,877,878],{"class":393}," md5sum\n",[329,880,881,883,885,887,890,892,894],{"class":158,"line":371},[329,882,861],{"class":393},[329,884,864],{"class":374},[329,886,867],{"class":374},[329,888,889],{"class":352}," 'User-Agent: Mozilla\u002F5.0 (Windows…)'",[329,891,873],{"class":352},[329,893,408],{"class":341},[329,895,878],{"class":393},[329,897,898],{"class":158,"line":390},[329,899,900],{"class":334},"# Identical hashes mean the variants were never really different — you can drop\n",[329,902,903],{"class":158,"line":417},[329,904,905],{"class":334},"# the device split entirely. Different hashes confirm the normalised key is\n",[329,907,908],{"class":158,"line":423},[329,909,910],{"class":334},"# doing real work.\n",[15,912,913,914,916],{},"Add a monitor on ",[30,915,36],{}," for the routes you fixed. Middleware upgrades reintroduce these headers regularly, and the symptom — a slowly falling hit ratio — is easy to miss for weeks.",[45,918,920],{"id":919},"faq","FAQ",[922,923,926,930],"details",{"className":924},[925],"faq-item",[927,928,929],"summary",{},"Is Vary: Accept-Encoding always safe?",[15,931,932,933,935],{},"Yes, and it is required whenever you serve compressed responses, because a client that cannot accept Brotli must not be handed a Brotli body. Its cardinality is tiny — a handful of encoding combinations in practice — so it costs almost nothing in fragmentation. It is the one ",[30,934,36],{}," value you should expect to see on nearly every compressible response.",[922,937,939,942],{"className":938},[925],[927,940,941],{},"How do I serve device-specific HTML without wrecking the cache?",[15,943,944],{},"Normalise to a small device class at the edge and vary on that derived value, or better, serve one responsive document to everyone and let CSS handle the difference. The responsive route removes the problem instead of managing it, and it also removes an entire class of bugs where a misclassified device receives the wrong layout.",[922,946,948,951],{"className":947},[925],[927,949,950],{},"Does Vary affect the browser cache too?",[15,952,953,954,956],{},"Yes — it applies to every cache in the chain, including the browser's. A response with ",[30,955,87],{}," will miss in the private cache as soon as any cookie changes, so a visitor who receives a new analytics identifier re-downloads a page they already had. The fragmentation cost is therefore paid twice: once at the edge, once per user.",[922,958,960,963],{"className":959},[925],[927,961,962],{},"What hit ratio should I expect after fixing this?",[15,964,965],{},"For content-hashed static assets, essentially 100% once the cache is warm — anything less means the key is still fragmented or the TTL is too short. For HTML the ceiling depends on how much of your traffic is cacheable at all: a fully static site can reach the high nineties, while a site with personalised sections will sit lower by design. Judge the number against the theoretical maximum for that path rather than against a universal target.",[922,967,969,972],{"className":968},[925],[927,970,971],{},"Can I remove Vary entirely on static assets?",[15,973,974,975,978,979,982],{},"Not if you serve compressed responses — omitting ",[30,976,977],{},"Vary: Accept-Encoding"," risks handing a Brotli body to a client that cannot decode it, via any shared cache in between. What you can do is ensure it is the ",[275,980,981],{},"only"," value present. Some CDNs normalise encoding internally and manage the variants themselves, in which case the header is still correct to send and simply costs nothing.",[922,984,986,989],{"className":985},[925],[927,987,988],{},"How do I catch a bad Vary before it reaches production?",[15,990,991,992,994,995,997,998,1000],{},"Assert on the response headers in the same smoke test that checks status codes after a deploy. A short list of routes with their expected ",[30,993,32],{}," and ",[30,996,36],{}," values, checked with a HEAD request, catches the middleware upgrade that quietly adds ",[30,999,827],{}," to the list. The alternative is noticing weeks later when someone finally asks why origin traffic has been climbing.",[45,1002,1004],{"id":1003},"related","Related",[50,1006,1007,1012,1019],{},[53,1008,1009,1011],{},[19,1010,22],{"href":21}," — cache keys, TTLs and hit-ratio fundamentals.",[53,1013,1014,1018],{},[19,1015,1017],{"href":1016},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fhttp-cache-control-headers-explained\u002F","HTTP Cache-Control Headers Explained"," — the directives that decide storage and freshness once the key is right.",[53,1020,1021,1024],{},[19,1022,1023],{"href":831},"Stale-While-Revalidate Implementation"," — serve the cached shell instantly while refreshing it behind the request.",[1026,1027,1029],"script",{"type":1028},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Recover CDN hit rate lost to a wide Vary header\",\n  \"description\": \"Audit the Vary values in production, narrow them to headers that genuinely change the response bytes, replace high-cardinality headers with a normalised cache key, and split personalised fragments out of cacheable HTML.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"position\": 1, \"name\": \"Audit Vary in production\", \"text\": \"Inspect the Vary header on key routes at both the edge and the origin.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 2, \"name\": \"Narrow to what changes the bytes\", \"text\": \"Reduce Vary to Accept-Encoding, plus Accept where formats are negotiated.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 3, \"name\": \"Normalise the cache key\", \"text\": \"Derive a low-cardinality value such as a device class at the edge and vary on that instead of the raw header.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 4, \"name\": \"Split personalised fragments\", \"text\": \"Cache the shared shell and fetch per-user content separately so nothing needs to vary on Cookie.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate\u002F#step-by-step-resolution\" }\n  ]\n}\n",[1026,1031,1032],{"type":1028},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How a Single Vary Header Destroys Your CDN Hit Rate\",\n  \"description\": \"High-cardinality Vary values fragment the edge cache into near-identical objects that never warm. Diagnose, normalise and verify without serving the wrong response.\",\n  \"datePublished\": \"2026-08-01\",\n  \"dateModified\": \"2026-08-01\",\n  \"author\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"publisher\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"mainEntityOfPage\": { \"@type\": \"WebPage\", \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate\u002F\" }\n}\n",[1026,1034,1035],{"type":1028},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    { \"@type\": \"ListItem\", \"position\": 1, \"name\": \"Home\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 2, \"name\": \"Advanced Caching Strategies & CDN Architecture\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"CDN Edge Caching Configuration\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Vary Header Pitfalls That Destroy CDN Hit Rate\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate\u002F\" }\n  ]\n}\n",[1037,1038,1039],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}",{"title":325,"searchDepth":338,"depth":338,"links":1041},[1042,1043,1044,1045,1046,1047],{"id":47,"depth":338,"text":48},{"id":263,"depth":338,"text":264},{"id":312,"depth":338,"text":313},{"id":836,"depth":338,"text":837},{"id":919,"depth":338,"text":920},{"id":1003,"depth":338,"text":1004},"One header in the Vary list can turn a 96% hit ratio into 40%. How cache-key fragmentation happens and how to normalise it without serving the wrong response.","md",{"slug":1051,"type":1052,"breadcrumb":1053,"datePublished":1061,"dateModified":1061},"vary-header-pitfalls-that-destroy-cdn-hit-rate","article",[1054,1057,1058,1059],{"name":1055,"url":1056},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1060},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate\u002F","2026-08-01",true,"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate",{"title":5,"description":1065},"Vary: User-Agent or Cookie fragments the edge cache into thousands of near-identical objects. Normalise the cache key, split what varies, and recover the hit ratio.","advanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fvary-header-pitfalls-that-destroy-cdn-hit-rate\u002Findex","U7ugO_0dgfvErmXIwRsGypNmjvYtL3p58yfTa8d-v20",[1069,1073],{"title":1070,"path":1071,"stem":1072},"Configuring stale-if-error for origin outages","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fconfiguring-stale-if-error-for-origin-outages","advanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fconfiguring-stale-if-error-for-origin-outages\u002Findex",{"title":1017,"path":1074,"stem":1075},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fhttp-cache-control-headers-explained","advanced-caching-strategies-cdn-architecture\u002Fhttp-cache-control-headers-explained\u002Findex",1785622406627]