[{"data":1,"prerenderedAt":1187},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F":1180},{"id":4,"title":5,"body":6,"description":1162,"extension":1163,"meta":1164,"navigation":447,"path":1174,"seo":1175,"stem":1178,"__hash__":1179},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002Findex.md","Invalidating immutable hashed assets safely",{"type":7,"value":8,"toc":1145},"minimark",[9,13,33,53,181,186,189,293,297,302,323,327,336,340,350,354,360,364,367,371,374,512,524,533,537,540,735,744,748,751,812,820,824,827,919,924,928,931,1072,1100,1104,1136,1141],[10,11,5],"h1",{"id":12},"invalidating-immutable-hashed-assets-safely",[14,15,16,17,22,23,27,28,32],"p",{},"This scenario sits under ",[18,19,21],"a",{"href":20},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F","Cache Invalidation Patterns"," within ",[18,24,26],{"href":25},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",": you serve hashed JS\u002FCSS with ",[29,30,31],"code",{},"Cache-Control: immutable"," and want new builds to take effect without a stale HTML document pointing at hashes that no longer exist.",[14,34,35,36,40,41,44,45,48,49,52],{},"Immutable hashed assets are supposed to make invalidation free — a new build produces new filenames, so there is nothing to purge. The trap is the ",[37,38,39],"em",{},"entry point",". The browser still has to download an ",[29,42,43],{},"index.html"," (or SSR shell) that lists the current hashes. If that document is cached too aggressively, a user fetches yesterday's HTML, which references ",[29,46,47],{},"app.8f3a9c.js"," — a file the new deploy deleted. The result is a ",[29,50,51],{},"404",", a white screen, or a \"ChunkLoadError\" with no obvious cause. This is invalidation-by-omission failing because the omission was only half-applied.",[14,54,55],{},[56,57,64,65,64,69,64,73,64,83,64,90,64,96,64,103,64,109,64,114,64,121,64,125,64,130,64,136,64,141,64,145,64,149,64,152,64,156,64,160,64,163,64,165,64,169,64,171,64,173,64,176,64],"svg",{"xmlns":58,"viewBox":59,"width":60,"role":61,"ariaLabel":62,"style":63},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 290","100%","img","Before and after showing a frozen HTML document referencing a deleted hash versus a revalidating document referencing the live hash","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:#001d3d"," ",[66,67,68],"title",{},"HTML \u002F hashed-asset skew",[70,71,72],"desc",{},"An immutable index points at a deleted hash and 404s; a no-cache index revalidates and points at the live hash.",[74,75],"rect",{"x":76,"y":76,"width":77,"height":78,"rx":79,"fill":80,"stroke":81,"style":82},"1","758","288","10","none","currentColor","stroke-opacity:0.18",[84,85,89],"text",{"x":86,"y":87,"fill":81,"style":88},"24","36","font-size:17px;font-weight:700","Why the entry point must revalidate",[84,91,95],{"x":92,"y":93,"fill":81,"style":94},"44","74","font-size:13px;font-weight:600","Broken: immutable index.html",[74,97],{"x":92,"y":98,"width":99,"height":100,"rx":101,"fill":81,"stroke":81,"style":102},"88","210","50","6","stroke-opacity:0.4;fill-opacity:0.06",[84,104,108],{"x":105,"y":106,"fill":81,"style":107},"149","110","font-size:12px;font-weight:600;text-anchor:middle","stale index.html",[84,110,113],{"x":105,"y":111,"fill":81,"style":112},"128","font-size:12px;text-anchor:middle","refs app.8f3a9c.js",[115,116],"line",{"x1":117,"y1":118,"x2":119,"y2":118,"stroke":81,"style":120},"254","113","304","stroke-opacity:0.5",[74,122],{"x":119,"y":98,"width":99,"height":100,"rx":101,"fill":123,"stroke":123,"style":124},"#0466c8","fill-opacity:0.12",[84,126,129],{"x":127,"y":128,"fill":81,"style":107},"409","118","deleted hash",[115,131],{"x1":132,"y1":118,"x2":133,"y2":118,"stroke":134,"style":135},"514","564","#b8860b","stroke-opacity:0.7",[74,137],{"x":133,"y":98,"width":138,"height":100,"rx":101,"fill":139,"stroke":134,"style":140},"150","#ffc300","fill-opacity:0.22",[84,142,144],{"x":143,"y":128,"fill":81,"style":107},"639","404 \u002F white screen",[84,146,148],{"x":92,"y":147,"fill":81,"style":94},"172","Fixed: no-cache index.html",[74,150],{"x":92,"y":151,"width":99,"height":100,"rx":101,"fill":123,"stroke":123,"style":124},"186",[84,153,155],{"x":105,"y":154,"fill":81,"style":107},"208","fresh index.html",[84,157,159],{"x":105,"y":158,"fill":81,"style":112},"226","refs app.7b2e1d.js",[115,161],{"x1":117,"y1":162,"x2":119,"y2":162,"stroke":81,"style":120},"211",[74,164],{"x":119,"y":151,"width":99,"height":100,"rx":101,"fill":123,"stroke":123,"style":124},[84,166,168],{"x":127,"y":167,"fill":81,"style":107},"216","live immutable hash",[115,170],{"x1":132,"y1":162,"x2":133,"y2":162,"stroke":81,"style":120},[74,172],{"x":133,"y":151,"width":138,"height":100,"rx":101,"fill":139,"stroke":134,"style":140},[84,174,175],{"x":143,"y":167,"fill":81,"style":107},"200 OK",[84,177,180],{"x":86,"y":178,"fill":81,"style":179},"270","font-size:12px;fill-opacity:0.85","Immutable for content-addressed assets; revalidate the mutable pointer.",[182,183,185],"h2",{"id":184},"rapid-diagnosis-htmlasset-hash-skew","Rapid Diagnosis: HTML\u002FAsset Hash Skew",[14,187,188],{},"Work this DevTools checklist when users report a blank page or chunk errors after a deploy:",[190,191,192,211,231,241,263,275],"ul",{},[193,194,195,199,200,202,203,206,207,210],"li",{},[196,197,198],"strong",{},"Network tab → reload."," Look for ",[29,201,51],{},"s on ",[29,204,205],{},".js","\u002F",[29,208,209],{},".css"," requests. A 404 on a hashed chunk is the signature of HTML\u002Fasset skew.",[193,212,213,64,219,222,223,226,227,230],{},[196,214,215,216,218],{},"Read the ",[29,217,43],{}," response headers.",[29,220,221],{},"curl -sI https:\u002F\u002Fyour-domain.com\u002F",". If you see ",[29,224,225],{},"max-age=31536000"," or ",[29,228,229],{},"immutable"," on the document, that is the bug — the entry point is frozen.",[193,232,233,236,237,240],{},[196,234,235],{},"Compare referenced hashes to deployed files."," View source on the served HTML, grab a script ",[29,238,239],{},"src",", and request it directly. A 404 confirms the HTML predates the current build.",[193,242,243,254,255,258,259,262],{},[196,244,245,246,249,250,253],{},"Check ",[29,247,248],{},"cf-cache-status"," \u002F ",[29,251,252],{},"x-cache"," on the HTML."," A ",[29,256,257],{},"HIT"," with a high ",[29,260,261],{},"Age"," means the edge is serving a stale document.",[193,264,265,64,268,226,271,274],{},[196,266,267],{},"Console.",[29,269,270],{},"ChunkLoadError",[29,272,273],{},"Loading chunk N failed"," from Webpack\u002FVite runtimes confirms a missing dynamically-imported hash.",[193,276,277,64,280,283,284,286,287,289,290,292],{},[196,278,279],{},"Target thresholds:",[29,281,282],{},"0"," 404s on hashed assets post-deploy, document ",[29,285,261],{}," resetting to near-zero after deploy, hashed assets holding ",[29,288,229],{}," with ",[29,291,225],{},".",[182,294,296],{"id":295},"root-cause-analysis","Root Cause Analysis",[298,299,301],"h3",{"id":300},"_1-the-html-document-is-cached-as-aggressively-as-the-assets","1. The HTML document is cached as aggressively as the assets",[14,303,304,305,307,308,311,312,314,315,318,319,322],{},"The most common cause. A blanket rule applied ",[29,306,229],{}," or a long ",[29,309,310],{},"max-age"," to everything, including ",[29,313,43],{},". The browser and edge now serve an old document that references deleted hashes. Immutable is correct for the ",[37,316,317],{},"content-addressed"," assets and wrong for the ",[37,320,321],{},"mutable"," pointer to them.",[298,324,326],{"id":325},"_2-the-service-worker-precached-a-stale-index","2. The Service Worker precached a stale index",[14,328,329,330,332,333,335],{},"Even with correct HTML headers, a Service Worker running a cache-first strategy on ",[29,331,206],{}," serves its precached ",[29,334,43],{}," indefinitely. The edge is fresh; the SW shadows it. Returning visitors hit old hashes while new visitors are fine — a maddeningly partial outage.",[298,337,339],{"id":338},"_3-old-hashed-files-were-deleted-before-stale-clients-stopped-requesting-them","3. Old hashed files were deleted before stale clients stopped requesting them",[14,341,342,343,346,347,349],{},"Immutable assets must remain ",[37,344,345],{},"retrievable"," for as long as any cached HTML can reference them. If the deploy wipes the previous build's files immediately, every client still holding the old ",[29,348,43],{}," 404s until its document expires. Atomic-swap deploys that delete the old directory cause this.",[298,351,353],{"id":352},"_4-atomic-deploy-ordering-html-published-before-assets-land","4. Atomic deploy ordering — HTML published before assets land",[14,355,356,357,359],{},"If the new ",[29,358,43],{}," goes live before all its referenced chunks finish uploading to the origin\u002Fedge, the first requests reference hashes that do not exist yet. A race window, but under high traffic it reliably produces 404s.",[182,361,363],{"id":362},"step-by-step-resolution","Step-by-Step Resolution",[14,365,366],{},"Fixes are ordered by impact: stop freezing the HTML first, then coordinate the SW, then keep old assets alive, then fix deploy ordering.",[298,368,370],{"id":369},"step-1-cache-the-html-for-revalidation-not-for-freshness","Step 1 — Cache the HTML for revalidation, not for freshness",[14,372,373],{},"The entry document must always check for a newer version. Apply short, revalidating headers to HTML while keeping assets immutable.",[375,376,381],"pre",{"className":377,"code":378,"language":379,"meta":380,"style":380},"language-nginx shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","# index.html and route documents: always revalidate; never immutable\nlocation = \u002Findex.html { add_header Cache-Control \"no-cache\"; }\nlocation ~* \u002F$       { add_header Cache-Control \"no-cache\"; }\n\n# Hashed assets: immutable, one year\nlocation ~* \\.[0-9a-f]{8,}\\.(js|css|woff2)$ {\n  add_header Cache-Control \"public, max-age=31536000, immutable\";\n}\n# trade-off: no-cache forces a conditional GET on every navigation, adding\n# one round-trip. That's the price of never serving stale HTML. Don't push\n# it to max-age=0 with must-revalidate unless you also need to forbid the\n# brief stale-serving the edge might otherwise do under load.\n","nginx","",[29,382,383,391,421,442,449,455,468,482,488,494,500,506],{"__ignoreMap":380},[384,385,387],"span",{"class":115,"line":386},1,[384,388,390],{"class":389},"sIIH1","# index.html and route documents: always revalidate; never immutable\n",[384,392,394,398,401,405,409,412,415,418],{"class":115,"line":393},2,[384,395,397],{"class":396},"sP5qI","location",[384,399,400],{"class":396}," =",[384,402,404],{"class":403},"s-_DF"," \u002Findex.html ",[384,406,408],{"class":407},"syybb","{",[384,410,411],{"class":396}," add_header ",[384,413,414],{"class":407},"Cache-Control ",[384,416,417],{"class":403},"\"no-cache\"",[384,419,420],{"class":407},"; }\n",[384,422,424,426,429,432,434,436,438,440],{"class":115,"line":423},3,[384,425,397],{"class":396},[384,427,428],{"class":396}," ~*",[384,430,431],{"class":403}," \u002F$       ",[384,433,408],{"class":407},[384,435,411],{"class":396},[384,437,414],{"class":407},[384,439,417],{"class":403},[384,441,420],{"class":407},[384,443,445],{"class":115,"line":444},4,[384,446,448],{"emptyLinePlaceholder":447},true,"\n",[384,450,452],{"class":115,"line":451},5,[384,453,454],{"class":389},"# Hashed assets: immutable, one year\n",[384,456,458,460,462,465],{"class":115,"line":457},6,[384,459,397],{"class":396},[384,461,428],{"class":396},[384,463,464],{"class":403}," \\.[0-9a-f]",[384,466,467],{"class":407},"{8,}\\.(js|css|woff2)$ {\n",[384,469,471,474,476,479],{"class":115,"line":470},7,[384,472,473],{"class":396},"  add_header ",[384,475,414],{"class":407},[384,477,478],{"class":403},"\"public, max-age=31536000, immutable\"",[384,480,481],{"class":407},";\n",[384,483,485],{"class":115,"line":484},8,[384,486,487],{"class":407},"}\n",[384,489,491],{"class":115,"line":490},9,[384,492,493],{"class":389},"# trade-off: no-cache forces a conditional GET on every navigation, adding\n",[384,495,497],{"class":115,"line":496},10,[384,498,499],{"class":389},"# one round-trip. That's the price of never serving stale HTML. Don't push\n",[384,501,503],{"class":115,"line":502},11,[384,504,505],{"class":389},"# it to max-age=0 with must-revalidate unless you also need to forbid the\n",[384,507,509],{"class":115,"line":508},12,[384,510,511],{"class":389},"# brief stale-serving the edge might otherwise do under load.\n",[14,513,514,515,519,520,292],{},"The directive split here — immutable for content-addressed assets, revalidating for the mutable pointer — is the core of ",[18,516,518],{"href":517},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fhttp-cache-control-headers-explained\u002Fsetting-up-immutable-cache-headers-for-hashed-assets\u002F","setting up immutable cache headers for hashed assets"," and the precedence rules in ",[18,521,523],{"href":522},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fhttp-cache-control-headers-explained\u002F","HTTP Cache-Control Headers Explained",[14,525,526,529,530,532],{},[196,527,528],{},"Expected outcome:"," the next navigation after a deploy fetches the new HTML (or a ",[29,531,119],{},"), eliminating references to deleted hashes — 404s on hashed assets drop to zero.",[298,534,536],{"id":535},"step-2-make-the-service-worker-network-first-for-navigations","Step 2 — Make the Service Worker network-first for navigations",[14,538,539],{},"A cache-first SW on the document re-introduces the skew Step 1 just fixed. Serve the document network-first so the SW prefers fresh HTML, falling back to cache only offline.",[375,541,545],{"className":542,"code":543,"language":544,"meta":380,"style":380},"language-javascript shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","\u002F\u002F Navigation requests: network-first so the document is always current\nself.addEventListener('fetch', (event) => {\n  if (event.request.mode === 'navigate') {\n    event.respondWith(\n      fetch(event.request).catch(() => caches.match('\u002Foffline.html'))\n    );\n    return;\n  }\n  \u002F\u002F Hashed assets stay cache-first — their URL changes when content does\n  event.respondWith(\n    caches.match(event.request).then((hit) => hit || fetch(event.request))\n  );\n  \u002F\u002F trade-off: network-first navigations add a network hit on each load and\n  \u002F\u002F lose instant offline navigation. Keep it for the document only; cache-first\n  \u002F\u002F is correct for immutable assets and would be wrong to apply to HTML.\n});\n","javascript",[29,546,547,552,583,600,611,641,646,653,658,663,672,706,711,717,723,729],{"__ignoreMap":380},[384,548,549],{"class":115,"line":386},[384,550,551],{"class":389},"\u002F\u002F Navigation requests: network-first so the document is always current\n",[384,553,554,557,561,564,567,570,574,577,580],{"class":115,"line":393},[384,555,556],{"class":407},"self.",[384,558,560],{"class":559},"ssM3C","addEventListener",[384,562,563],{"class":407},"(",[384,565,566],{"class":403},"'fetch'",[384,568,569],{"class":407},", (",[384,571,573],{"class":572},"seIZK","event",[384,575,576],{"class":407},") ",[384,578,579],{"class":396},"=>",[384,581,582],{"class":407}," {\n",[384,584,585,588,591,594,597],{"class":115,"line":423},[384,586,587],{"class":396},"  if",[384,589,590],{"class":407}," (event.request.mode ",[384,592,593],{"class":396},"===",[384,595,596],{"class":403}," 'navigate'",[384,598,599],{"class":407},") {\n",[384,601,602,605,608],{"class":115,"line":444},[384,603,604],{"class":407},"    event.",[384,606,607],{"class":559},"respondWith",[384,609,610],{"class":407},"(\n",[384,612,613,616,619,622,625,627,630,633,635,638],{"class":115,"line":451},[384,614,615],{"class":559},"      fetch",[384,617,618],{"class":407},"(event.request).",[384,620,621],{"class":559},"catch",[384,623,624],{"class":407},"(() ",[384,626,579],{"class":396},[384,628,629],{"class":407}," caches.",[384,631,632],{"class":559},"match",[384,634,563],{"class":407},[384,636,637],{"class":403},"'\u002Foffline.html'",[384,639,640],{"class":407},"))\n",[384,642,643],{"class":115,"line":457},[384,644,645],{"class":407},"    );\n",[384,647,648,651],{"class":115,"line":470},[384,649,650],{"class":396},"    return",[384,652,481],{"class":407},[384,654,655],{"class":115,"line":484},[384,656,657],{"class":407},"  }\n",[384,659,660],{"class":115,"line":490},[384,661,662],{"class":389},"  \u002F\u002F Hashed assets stay cache-first — their URL changes when content does\n",[384,664,665,668,670],{"class":115,"line":496},[384,666,667],{"class":407},"  event.",[384,669,607],{"class":559},[384,671,610],{"class":407},[384,673,674,677,679,681,684,687,690,692,694,697,700,703],{"class":115,"line":502},[384,675,676],{"class":407},"    caches.",[384,678,632],{"class":559},[384,680,618],{"class":407},[384,682,683],{"class":559},"then",[384,685,686],{"class":407},"((",[384,688,689],{"class":572},"hit",[384,691,576],{"class":407},[384,693,579],{"class":396},[384,695,696],{"class":407}," hit ",[384,698,699],{"class":396},"||",[384,701,702],{"class":559}," fetch",[384,704,705],{"class":407},"(event.request))\n",[384,707,708],{"class":115,"line":508},[384,709,710],{"class":407},"  );\n",[384,712,714],{"class":115,"line":713},13,[384,715,716],{"class":389},"  \u002F\u002F trade-off: network-first navigations add a network hit on each load and\n",[384,718,720],{"class":115,"line":719},14,[384,721,722],{"class":389},"  \u002F\u002F lose instant offline navigation. Keep it for the document only; cache-first\n",[384,724,726],{"class":115,"line":725},15,[384,727,728],{"class":389},"  \u002F\u002F is correct for immutable assets and would be wrong to apply to HTML.\n",[384,730,732],{"class":115,"line":731},16,[384,733,734],{"class":407},"});\n",[14,736,737,739,740,292],{},[196,738,528],{}," returning visitors get the current document on the next online navigation; the partial outage where only repeat users break disappears. Full SW coordination is covered in ",[18,741,743],{"href":742},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002F","Service Worker Caching Strategies",[298,745,747],{"id":746},"step-3-keep-the-previous-builds-assets-alive-across-deploys","Step 3 — Keep the previous build's assets alive across deploys",[14,749,750],{},"Retain old hashed files long enough for stale documents to expire. Deploy additively rather than replacing the asset directory.",[375,752,756],{"className":753,"code":754,"language":755,"meta":380,"style":380},"language-bash shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","# Sync new assets WITHOUT deleting the previous build's hashed files\naws s3 sync .\u002Fdist\u002Fassets s3:\u002F\u002Fmy-bucket\u002Fassets\u002F \\\n  --cache-control \"public, max-age=31536000, immutable\"\n# (no --delete) then prune builds older than the longest possible HTML Age\n# trade-off: keeping old assets costs storage and leaves stale chunks\n# fetchable. Don't keep them forever — prune anything older than your\n# HTML revalidation window once no client can still reference it.\n","bash",[29,757,758,763,783,792,797,802,807],{"__ignoreMap":380},[384,759,760],{"class":115,"line":386},[384,761,762],{"class":389},"# Sync new assets WITHOUT deleting the previous build's hashed files\n",[384,764,765,768,771,774,777,780],{"class":115,"line":393},[384,766,767],{"class":572},"aws",[384,769,770],{"class":403}," s3",[384,772,773],{"class":403}," sync",[384,775,776],{"class":403}," .\u002Fdist\u002Fassets",[384,778,779],{"class":403}," s3:\u002F\u002Fmy-bucket\u002Fassets\u002F",[384,781,782],{"class":396}," \\\n",[384,784,785,789],{"class":115,"line":423},[384,786,788],{"class":787},"sf6mN","  --cache-control",[384,790,791],{"class":403}," \"public, max-age=31536000, immutable\"\n",[384,793,794],{"class":115,"line":444},[384,795,796],{"class":389},"# (no --delete) then prune builds older than the longest possible HTML Age\n",[384,798,799],{"class":115,"line":451},[384,800,801],{"class":389},"# trade-off: keeping old assets costs storage and leaves stale chunks\n",[384,803,804],{"class":115,"line":457},[384,805,806],{"class":389},"# fetchable. Don't keep them forever — prune anything older than your\n",[384,808,809],{"class":115,"line":470},[384,810,811],{"class":389},"# HTML revalidation window once no client can still reference it.\n",[14,813,814,816,817,819],{},[196,815,528],{}," clients holding an old ",[29,818,43],{}," continue to load successfully until they pick up the new document; no transitional 404s.",[298,821,823],{"id":822},"step-4-publish-assets-before-html-atomically","Step 4 — Publish assets before HTML, atomically",[14,825,826],{},"Order the deploy so every chunk exists before the document that references it goes live. Upload assets first, verify, then flip the HTML.",[375,828,830],{"className":753,"code":829,"language":755,"meta":380,"style":380},"# 1. Upload assets, 2. verify a sample chunk, 3. only then publish HTML\naws s3 sync .\u002Fdist\u002Fassets s3:\u002F\u002Fmy-bucket\u002Fassets\u002F --cache-control \"public, max-age=31536000, immutable\"\ncurl -fsSI \"https:\u002F\u002Fyour-domain.com\u002Fassets\u002F$(ls dist\u002Fassets | grep -m1 '\\.js$')\" >\u002Fdev\u002Fnull\naws s3 cp .\u002Fdist\u002Findex.html s3:\u002F\u002Fmy-bucket\u002Findex.html --cache-control \"no-cache\"\n# trade-off: the verify step adds deploy time. Skip it only for low-traffic\n# sites where the upload race window is unlikely to be hit by a real user.\n",[29,831,832,837,854,889,909,914],{"__ignoreMap":380},[384,833,834],{"class":115,"line":386},[384,835,836],{"class":389},"# 1. Upload assets, 2. verify a sample chunk, 3. only then publish HTML\n",[384,838,839,841,843,845,847,849,852],{"class":115,"line":393},[384,840,767],{"class":572},[384,842,770],{"class":403},[384,844,773],{"class":403},[384,846,776],{"class":403},[384,848,779],{"class":403},[384,850,851],{"class":787}," --cache-control",[384,853,791],{"class":403},[384,855,856,859,862,865,868,871,874,877,880,883,886],{"class":115,"line":423},[384,857,858],{"class":572},"curl",[384,860,861],{"class":787}," -fsSI",[384,863,864],{"class":403}," \"https:\u002F\u002Fyour-domain.com\u002Fassets\u002F$(",[384,866,867],{"class":572},"ls",[384,869,870],{"class":403}," dist\u002Fassets ",[384,872,873],{"class":396},"|",[384,875,876],{"class":572}," grep",[384,878,879],{"class":787}," -m1",[384,881,882],{"class":403}," '\\.js$')\"",[384,884,885],{"class":396}," >",[384,887,888],{"class":403},"\u002Fdev\u002Fnull\n",[384,890,891,893,895,898,901,904,906],{"class":115,"line":444},[384,892,767],{"class":572},[384,894,770],{"class":403},[384,896,897],{"class":403}," cp",[384,899,900],{"class":403}," .\u002Fdist\u002Findex.html",[384,902,903],{"class":403}," s3:\u002F\u002Fmy-bucket\u002Findex.html",[384,905,851],{"class":787},[384,907,908],{"class":403}," \"no-cache\"\n",[384,910,911],{"class":115,"line":451},[384,912,913],{"class":389},"# trade-off: the verify step adds deploy time. Skip it only for low-traffic\n",[384,915,916],{"class":115,"line":457},[384,917,918],{"class":389},"# sites where the upload race window is unlikely to be hit by a real user.\n",[14,920,921,923],{},[196,922,528],{}," the race window where HTML references not-yet-uploaded hashes is closed; first-request 404s during deploy are eliminated.",[182,925,927],{"id":926},"verification","Verification",[14,929,930],{},"Confirm the fix with a before\u002Fafter edge check and a field signal:",[375,932,934],{"className":753,"code":933,"language":755,"meta":380,"style":380},"# HTML must revalidate; assets must stay immutable\ncurl -sI https:\u002F\u002Fyour-domain.com\u002F | grep -i cache-control   # expect: no-cache\nasset=$(curl -s https:\u002F\u002Fyour-domain.com\u002F | grep -oE 'assets\u002F[^\"]+\\.js' | head -1)\ncurl -sI \"https:\u002F\u002Fyour-domain.com\u002F$asset\" | grep -i cache-control  # expect: immutable, max-age=31536000\ncurl -s -o \u002Fdev\u002Fnull -w '%{http_code}\\n' \"https:\u002F\u002Fyour-domain.com\u002F$asset\"  # expect: 200\n# trade-off: this validates the served document only. It won't catch a\n# Service-Worker-shadowed stale index — test that with a returning-user\n# session in an incognito-then-revisit flow, not curl.\n",[29,935,936,941,965,1004,1030,1057,1062,1067],{"__ignoreMap":380},[384,937,938],{"class":115,"line":386},[384,939,940],{"class":389},"# HTML must revalidate; assets must stay immutable\n",[384,942,943,945,948,951,954,956,959,962],{"class":115,"line":393},[384,944,858],{"class":572},[384,946,947],{"class":787}," -sI",[384,949,950],{"class":403}," https:\u002F\u002Fyour-domain.com\u002F",[384,952,953],{"class":396}," |",[384,955,876],{"class":572},[384,957,958],{"class":787}," -i",[384,960,961],{"class":403}," cache-control",[384,963,964],{"class":389},"   # expect: no-cache\n",[384,966,967,970,973,976,978,981,983,985,987,990,993,995,998,1001],{"class":115,"line":423},[384,968,969],{"class":407},"asset",[384,971,972],{"class":396},"=",[384,974,975],{"class":407},"$(",[384,977,858],{"class":572},[384,979,980],{"class":787}," -s",[384,982,950],{"class":403},[384,984,953],{"class":396},[384,986,876],{"class":572},[384,988,989],{"class":787}," -oE",[384,991,992],{"class":403}," 'assets\u002F[^\"]+\\.js'",[384,994,953],{"class":396},[384,996,997],{"class":572}," head",[384,999,1000],{"class":787}," -1",[384,1002,1003],{"class":407},")\n",[384,1005,1006,1008,1010,1013,1016,1019,1021,1023,1025,1027],{"class":115,"line":444},[384,1007,858],{"class":572},[384,1009,947],{"class":787},[384,1011,1012],{"class":403}," \"https:\u002F\u002Fyour-domain.com\u002F",[384,1014,1015],{"class":407},"$asset",[384,1017,1018],{"class":403},"\"",[384,1020,953],{"class":396},[384,1022,876],{"class":572},[384,1024,958],{"class":787},[384,1026,961],{"class":403},[384,1028,1029],{"class":389},"  # expect: immutable, max-age=31536000\n",[384,1031,1032,1034,1036,1039,1042,1045,1048,1050,1052,1054],{"class":115,"line":451},[384,1033,858],{"class":572},[384,1035,980],{"class":787},[384,1037,1038],{"class":787}," -o",[384,1040,1041],{"class":403}," \u002Fdev\u002Fnull",[384,1043,1044],{"class":787}," -w",[384,1046,1047],{"class":403}," '%{http_code}\\n'",[384,1049,1012],{"class":403},[384,1051,1015],{"class":407},[384,1053,1018],{"class":403},[384,1055,1056],{"class":389},"  # expect: 200\n",[384,1058,1059],{"class":115,"line":457},[384,1060,1061],{"class":389},"# trade-off: this validates the served document only. It won't catch a\n",[384,1063,1064],{"class":115,"line":470},[384,1065,1066],{"class":389},"# Service-Worker-shadowed stale index — test that with a returning-user\n",[384,1068,1069],{"class":115,"line":484},[384,1070,1071],{"class":389},"# session in an incognito-then-revisit flow, not curl.\n",[14,1073,1074,1075,1078,1079,1081,1082,1085,1086,1088,1089,1091,1092,1094,1095,1099],{},"The passing state: the document returns ",[29,1076,1077],{},"no-cache"," (or a ",[29,1080,119],{}," on revalidation), every hash it references returns ",[29,1083,1084],{},"200",", and assets carry ",[29,1087,229],{},". In CI, assert these as a post-deploy gate alongside your tag-purge checks. In the field, watch RUM for a flat-line of zero ",[29,1090,270],{}," \u002F asset-404 beacons across the deploy boundary, and confirm document ",[29,1093,261],{}," resets near zero on the edge immediately after release. Soft-purging the HTML on deploy, paired with a short ",[18,1096,1098],{"href":1097},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002F","stale-while-revalidate"," window, smooths the transition further.",[182,1101,1103],{"id":1102},"related","Related",[190,1105,1106,1111,1117,1124,1129],{},[193,1107,1108,1110],{},[18,1109,21],{"href":20}," — where invalidation-by-omission fits among the four purge primitives.",[193,1112,1113,1116],{},[18,1114,1115],{"href":517},"Setting up immutable cache headers for hashed assets"," — the header config that makes omission safe.",[193,1118,1119,1123],{},[18,1120,1122],{"href":1121},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F","Purging CDN cache by tag on deploy"," — purging the mutable document while assets self-invalidate.",[193,1125,1126,1128],{},[18,1127,743],{"href":742}," — keeping the SW from shadowing a fresh index document.",[193,1130,1131,1135],{},[18,1132,1134],{"href":1133},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002F","CDN Edge Caching Configuration"," — edge TTLs and cache keys for the HTML entry point.",[1137,1138,1140],"script",{"type":1139},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@graph\": [\n    {\n      \"@type\": \"TechArticle\",\n      \"headline\": \"Invalidating immutable hashed assets safely\",\n      \"description\": \"Ship new immutable hashed bundles without serving stale HTML that references deleted hashes, by revalidating the document, coordinating the Service Worker, and ordering the deploy.\",\n      \"datePublished\": \"2026-06-18\",\n      \"dateModified\": \"2026-06-18\",\n      \"author\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n      \"mainEntityOfPage\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F\"\n    },\n    {\n      \"@type\": \"HowTo\",\n      \"name\": \"Invalidate immutable hashed assets without serving a stale index\",\n      \"step\": [\n        { \"@type\": \"HowToStep\", \"name\": \"Cache the HTML for revalidation, not freshness\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F#step-1--cache-the-html-for-revalidation-not-for-freshness\" },\n        { \"@type\": \"HowToStep\", \"name\": \"Make the Service Worker network-first for navigations\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F#step-2--make-the-service-worker-network-first-for-navigations\" },\n        { \"@type\": \"HowToStep\", \"name\": \"Keep the previous build's assets alive across deploys\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F#step-3--keep-the-previous-builds-assets-alive-across-deploys\" },\n        { \"@type\": \"HowToStep\", \"name\": \"Publish assets before HTML, atomically\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F#step-4--publish-assets-before-html-atomically\" }\n      ]\n    },\n    {\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\": \"Cache Invalidation Patterns\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F\" },\n        { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Invalidating immutable hashed assets safely\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F\" }\n      ]\n    }\n  ]\n}\n",[1142,1143,1144],"style",{},"html pre.shiki code .sIIH1, html code.shiki .sIIH1{--shiki-default:#66707B;--shiki-dark:#66707B;--shiki-light:#66707B}html pre.shiki code .sP5qI, html code.shiki .sP5qI{--shiki-default:#A0111F;--shiki-dark:#A0111F;--shiki-light:#A0111F}html pre.shiki code .s-_DF, html code.shiki .s-_DF{--shiki-default:#032563;--shiki-dark:#032563;--shiki-light:#032563}html pre.shiki code .syybb, html code.shiki .syybb{--shiki-default:#0E1116;--shiki-dark:#0E1116;--shiki-light:#0E1116}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 .ssM3C, html code.shiki .ssM3C{--shiki-default:#622CBC;--shiki-dark:#622CBC;--shiki-light:#622CBC}html pre.shiki code .seIZK, html code.shiki .seIZK{--shiki-default:#702C00;--shiki-dark:#702C00;--shiki-light:#702C00}html pre.shiki code .sf6mN, html code.shiki .sf6mN{--shiki-default:#023B95;--shiki-dark:#023B95;--shiki-light:#023B95}",{"title":380,"searchDepth":393,"depth":393,"links":1146},[1147,1148,1154,1160,1161],{"id":184,"depth":393,"text":185},{"id":295,"depth":393,"text":296,"children":1149},[1150,1151,1152,1153],{"id":300,"depth":423,"text":301},{"id":325,"depth":423,"text":326},{"id":338,"depth":423,"text":339},{"id":352,"depth":423,"text":353},{"id":362,"depth":393,"text":363,"children":1155},[1156,1157,1158,1159],{"id":369,"depth":423,"text":370},{"id":535,"depth":423,"text":536},{"id":746,"depth":423,"text":747},{"id":822,"depth":423,"text":823},{"id":926,"depth":393,"text":927},{"id":1102,"depth":393,"text":1103},"How to ship new immutable hashed bundles without a stale index document pointing at old or missing hashes.","md",{"slug":12,"type":1165,"breadcrumb":1166,"datePublished":1173,"dateModified":1173},"long_tail",[1167,1169,1170,1171],{"name":1168,"url":206},"Home",{"name":26,"url":25},{"name":21,"url":20},{"name":5,"url":1172},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F","2026-06-18","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely",{"title":1176,"description":1177},"Invalidating Immutable Hashed Assets Safely","Roll immutable content-hashed assets without serving stale HTML that references deleted hashes. Fix the skew between immutable bundles and the index document.","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002Findex","bqDiflLRVRUiO0PvBJhJx5uaGhcBo4efKx0RTHkX4WQ",[1181,1184],{"title":21,"path":1182,"stem":1183,"children":-1},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Findex",{"title":1122,"path":1185,"stem":1186,"children":-1},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002Findex",1782237170939]