[{"data":1,"prerenderedAt":1687},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely":1680},{"id":4,"title":5,"body":6,"description":1662,"extension":1663,"meta":1664,"navigation":447,"path":1674,"seo":1675,"stem":1678,"__hash__":1679},"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":1645},"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,1081,1085,1088,1195,1198,1339,1345,1573,1600,1604,1636,1641],[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.",[14,925,926],{},[56,927,64,930,64,933,64,936,64,953,64,956,64,959,64,963,64,968,64,973,64,977,64,983,64,986,64,990,64,993,64,998,64,1002,64,1006,64,1009,64,1013,64,1016,64,1020,64,1024,64,1028,64,1031,64,1035,64,1038,64,1042,64,1045,64,1048,64,1051,64,1055,64,1060,64,1064,64,1067,64,1072,64,1077,64],{"xmlns":58,"viewBox":928,"width":60,"role":61,"ariaLabel":929,"style":63},"0 0 760 322","Two deploy timelines: publishing HTML before its chunks land opens a 404 window, while uploading assets first then verifying then flipping the HTML keeps every referenced hash resolvable",[66,931,932],{},"Safe deploy ordering for immutable assets",[70,934,935],{},"A broken timeline flips index.html live while its chunks are still uploading, so the first requests 404. A safe timeline uploads the hashed assets first, verifies a sample chunk returns 200, then publishes the no-cache index.html and retains the previous build until the longest cached HTML Age expires, so every hash the document names already exists.",[937,938,64,939,64],"defs",{},[940,941,64,948,64],"marker",{"id":942,"viewBox":943,"refX":944,"refY":945,"markerWidth":946,"markerHeight":946,"orient":947},"iha-arrow","0 0 10 10","9","5","7","auto-start-reverse",[949,950],"path",{"d":951,"fill":81,"style":952},"M0 0 L10 5 L0 10 z","fill-opacity:0.55",[74,954],{"x":76,"y":76,"width":77,"height":955,"rx":79,"fill":80,"stroke":81,"style":82},"320",[84,957,958],{"x":86,"y":87,"fill":81,"style":88},"Publish assets before the HTML that names them",[84,960,962],{"x":86,"y":961,"fill":81,"style":94},"64","Broken order — document goes live before its chunks",[74,964],{"x":86,"y":965,"width":966,"height":100,"rx":101,"fill":123,"stroke":123,"style":967},"76","196","fill-opacity:0.14",[84,969,972],{"x":970,"y":971,"fill":81,"style":107},"122","98","Flip index.html",[84,974,159],{"x":970,"y":975,"fill":81,"style":976},"116","font-size:11px;text-anchor:middle",[115,978],{"x1":979,"y1":980,"x2":981,"y2":980,"stroke":81,"style":120,"markerEnd":982},"220","101","256","url(#iha-arrow)",[74,984],{"x":985,"y":965,"width":966,"height":100,"rx":101,"fill":139,"stroke":134,"style":140},"258",[84,987,989],{"x":988,"y":971,"fill":81,"style":107},"356","Chunks still uploading",[84,991,992],{"x":988,"y":975,"fill":81,"style":976},"not on the edge yet",[115,994],{"x1":995,"y1":980,"x2":996,"y2":980,"stroke":134,"style":997,"markerEnd":982},"454","490","stroke-opacity:0.6",[74,999],{"x":1000,"y":965,"width":1001,"height":100,"rx":101,"fill":139,"stroke":134,"style":140},"492","224",[84,1003,1005],{"x":1004,"y":971,"fill":81,"style":107},"604","First requests 404",[84,1007,1008],{"x":1004,"y":975,"fill":81,"style":976},"white screen \u002F ChunkLoadError",[84,1010,1012],{"x":86,"y":1011,"fill":81,"style":94},"162","Safe order — assets first, verify, then flip",[74,1014],{"x":86,"y":1015,"width":138,"height":100,"rx":101,"fill":123,"stroke":123,"style":967},"174",[84,1017,1019],{"x":1018,"y":966,"fill":81,"style":107},"99","Upload hashed",[84,1021,1023],{"x":1018,"y":1022,"fill":81,"style":976},"214","assets first",[115,1025],{"x1":1015,"y1":1026,"x2":1027,"y2":1026,"stroke":81,"style":120,"markerEnd":982},"199","190",[74,1029],{"x":1030,"y":1015,"width":138,"height":100,"rx":101,"fill":123,"stroke":123,"style":967},"192",[84,1032,1034],{"x":1033,"y":966,"fill":81,"style":107},"267","Verify a chunk",[84,1036,1037],{"x":1033,"y":1022,"fill":81,"style":976},"returns 200",[115,1039],{"x1":1040,"y1":1026,"x2":1041,"y2":1026,"stroke":81,"style":120,"markerEnd":982},"342","358",[74,1043],{"x":1044,"y":1015,"width":138,"height":100,"rx":101,"fill":123,"stroke":123,"style":967},"360",[84,1046,972],{"x":1047,"y":966,"fill":81,"style":107},"435",[84,1049,1050],{"x":1047,"y":1022,"fill":81,"style":976},"no-cache",[115,1052],{"x1":1053,"y1":1026,"x2":1054,"y2":1026,"stroke":81,"style":120,"markerEnd":982},"510","526",[74,1056],{"x":1057,"y":1015,"width":1058,"height":100,"rx":101,"fill":123,"stroke":123,"style":1059},"528","188","stroke-dasharray:5 4;fill-opacity:0.07;stroke-opacity:0.6",[84,1061,1063],{"x":1062,"y":966,"fill":81,"style":107},"622","Retain old build",[84,1065,1066],{"x":1062,"y":1022,"fill":81,"style":976},"until cached Age expires",[115,1068],{"x1":86,"y1":1069,"x2":1070,"y2":1069,"stroke":81,"style":1071},"252","736","stroke-opacity:0.3",[84,1073,1076],{"x":86,"y":1074,"fill":81,"style":1075},"278","font-size:12px","Every hash the document names already exists — 200 on the first request.",[84,1078,1080],{"x":86,"y":1079,"fill":81,"style":179},"302","Prune the prior build only once no cached HTML can still reference it.",[182,1082,1084],{"id":1083},"verification","Verification",[14,1086,1087],{},"The signal that the skew is gone is a clean before\u002Fafter diff across the deploy boundary — the entry document stops freezing and every hash it names resolves:",[1089,1090,1091,1107],"table",{},[1092,1093,1094],"thead",{},[1095,1096,1097,1101,1104],"tr",{},[1098,1099,1100],"th",{},"Signal",[1098,1102,1103],{},"Before (skewed)",[1098,1105,1106],{},"After (fixed)",[1108,1109,1110,1134,1150,1167,1180],"tbody",{},[1095,1111,1112,1121,1126],{},[1113,1114,1115,1118,1119],"td",{},[29,1116,1117],{},"Cache-Control"," on ",[29,1120,206],{},[1113,1122,1123],{},[29,1124,1125],{},"max-age=31536000, immutable",[1113,1127,1128,1130,1131,1133],{},[29,1129,1050],{}," (or ",[29,1132,119],{}," on revalidation)",[1095,1135,1136,1142,1145],{},[1113,1137,1138,1139,1141],{},"Document ",[29,1140,261],{}," after deploy",[1113,1143,1144],{},"high, never resets",[1113,1146,1147,1148],{},"resets to near ",[29,1149,282],{},[1095,1151,1152,1160,1163],{},[1113,1153,1154,1155,206,1157,1159],{},"404s on hashed ",[29,1156,205],{},[29,1158,209],{}," post-deploy",[1113,1161,1162],{},"spikes on every release",[1113,1164,1165],{},[29,1166,282],{},[1095,1168,1169,1174,1177],{},[1113,1170,1171,1173],{},[29,1172,270],{}," beacons in RUM",[1113,1175,1176],{},"spike at each deploy",[1113,1178,1179],{},"flat line across the boundary",[1095,1181,1182,1187,1190],{},[1113,1183,1184,1186],{},[29,1185,1117],{}," on hashed assets",[1113,1188,1189],{},"unchanged",[1113,1191,1192],{},[29,1193,1194],{},"immutable, max-age=31536000",[14,1196,1197],{},"Confirm the header split with a before\u002Fafter edge check and a field signal:",[375,1199,1201],{"className":753,"code":1200,"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,1202,1203,1208,1232,1271,1297,1324,1329,1334],{"__ignoreMap":380},[384,1204,1205],{"class":115,"line":386},[384,1206,1207],{"class":389},"# HTML must revalidate; assets must stay immutable\n",[384,1209,1210,1212,1215,1218,1221,1223,1226,1229],{"class":115,"line":393},[384,1211,858],{"class":572},[384,1213,1214],{"class":787}," -sI",[384,1216,1217],{"class":403}," https:\u002F\u002Fyour-domain.com\u002F",[384,1219,1220],{"class":396}," |",[384,1222,876],{"class":572},[384,1224,1225],{"class":787}," -i",[384,1227,1228],{"class":403}," cache-control",[384,1230,1231],{"class":389},"   # expect: no-cache\n",[384,1233,1234,1237,1240,1243,1245,1248,1250,1252,1254,1257,1260,1262,1265,1268],{"class":115,"line":423},[384,1235,1236],{"class":407},"asset",[384,1238,1239],{"class":396},"=",[384,1241,1242],{"class":407},"$(",[384,1244,858],{"class":572},[384,1246,1247],{"class":787}," -s",[384,1249,1217],{"class":403},[384,1251,1220],{"class":396},[384,1253,876],{"class":572},[384,1255,1256],{"class":787}," -oE",[384,1258,1259],{"class":403}," 'assets\u002F[^\"]+\\.js'",[384,1261,1220],{"class":396},[384,1263,1264],{"class":572}," head",[384,1266,1267],{"class":787}," -1",[384,1269,1270],{"class":407},")\n",[384,1272,1273,1275,1277,1280,1283,1286,1288,1290,1292,1294],{"class":115,"line":444},[384,1274,858],{"class":572},[384,1276,1214],{"class":787},[384,1278,1279],{"class":403}," \"https:\u002F\u002Fyour-domain.com\u002F",[384,1281,1282],{"class":407},"$asset",[384,1284,1285],{"class":403},"\"",[384,1287,1220],{"class":396},[384,1289,876],{"class":572},[384,1291,1225],{"class":787},[384,1293,1228],{"class":403},[384,1295,1296],{"class":389},"  # expect: immutable, max-age=31536000\n",[384,1298,1299,1301,1303,1306,1309,1312,1315,1317,1319,1321],{"class":115,"line":451},[384,1300,858],{"class":572},[384,1302,1247],{"class":787},[384,1304,1305],{"class":787}," -o",[384,1307,1308],{"class":403}," \u002Fdev\u002Fnull",[384,1310,1311],{"class":787}," -w",[384,1313,1314],{"class":403}," '%{http_code}\\n'",[384,1316,1279],{"class":403},[384,1318,1282],{"class":407},[384,1320,1285],{"class":403},[384,1322,1323],{"class":389},"  # expect: 200\n",[384,1325,1326],{"class":115,"line":457},[384,1327,1328],{"class":389},"# trade-off: this validates the served document only. It won't catch a\n",[384,1330,1331],{"class":115,"line":470},[384,1332,1333],{"class":389},"# Service-Worker-shadowed stale index — test that with a returning-user\n",[384,1335,1336],{"class":115,"line":484},[384,1337,1338],{"class":389},"# session in an incognito-then-revisit flow, not curl.\n",[14,1340,1341,1342,1344],{},"Lock the fix in with a post-deploy CI gate that fails the release if the document ever comes back ",[29,1343,229],{}," or any referenced hash 404s:",[375,1346,1348],{"className":753,"code":1347,"language":755,"meta":380,"style":380},"# post-deploy gate: fail the pipeline if HTML froze or a chunk went missing\nset -euo pipefail\ndoc=$(curl -sI https:\u002F\u002Fyour-domain.com\u002F | grep -i '^cache-control' || true)\necho \"$doc\" | grep -qi 'immutable' && { echo \"FAIL: index.html is immutable\"; exit 1; }\nfor a in $(curl -s https:\u002F\u002Fyour-domain.com\u002F | grep -oE 'assets\u002F[^\"]+\\.(js|css)'); do\n  code=$(curl -s -o \u002Fdev\u002Fnull -w '%{http_code}' \"https:\u002F\u002Fyour-domain.com\u002F$a\")\n  [ \"$code\" = \"200\" ] || { echo \"FAIL: $a -> $code\"; exit 1; }\ndone\n# trade-off: this greps the served HTML, so it can't see hashes behind a\n# client Service Worker. Pair it with a synthetic returning-user check when\n# a SW is in play; skip that only for sites with no Service Worker at all.\n",[29,1349,1350,1355,1366,1398,1440,1475,1506,1553,1558,1563,1568],{"__ignoreMap":380},[384,1351,1352],{"class":115,"line":386},[384,1353,1354],{"class":389},"# post-deploy gate: fail the pipeline if HTML froze or a chunk went missing\n",[384,1356,1357,1360,1363],{"class":115,"line":393},[384,1358,1359],{"class":787},"set",[384,1361,1362],{"class":787}," -euo",[384,1364,1365],{"class":403}," pipefail\n",[384,1367,1368,1371,1373,1375,1377,1379,1381,1383,1385,1387,1390,1393,1396],{"class":115,"line":423},[384,1369,1370],{"class":407},"doc",[384,1372,1239],{"class":396},[384,1374,1242],{"class":407},[384,1376,858],{"class":572},[384,1378,1214],{"class":787},[384,1380,1217],{"class":403},[384,1382,1220],{"class":396},[384,1384,876],{"class":572},[384,1386,1225],{"class":787},[384,1388,1389],{"class":403}," '^cache-control'",[384,1391,1392],{"class":396}," ||",[384,1394,1395],{"class":787}," true",[384,1397,1270],{"class":407},[384,1399,1400,1403,1406,1409,1411,1413,1415,1418,1421,1424,1426,1429,1432,1435,1438],{"class":115,"line":444},[384,1401,1402],{"class":787},"echo",[384,1404,1405],{"class":403}," \"",[384,1407,1408],{"class":407},"$doc",[384,1410,1285],{"class":403},[384,1412,1220],{"class":396},[384,1414,876],{"class":572},[384,1416,1417],{"class":787}," -qi",[384,1419,1420],{"class":403}," 'immutable'",[384,1422,1423],{"class":407}," && { ",[384,1425,1402],{"class":787},[384,1427,1428],{"class":403}," \"FAIL: index.html is immutable\"",[384,1430,1431],{"class":407},"; ",[384,1433,1434],{"class":787},"exit",[384,1436,1437],{"class":787}," 1",[384,1439,420],{"class":407},[384,1441,1442,1445,1448,1451,1454,1456,1458,1460,1462,1464,1466,1469,1472],{"class":115,"line":451},[384,1443,1444],{"class":396},"for",[384,1446,1447],{"class":407}," a ",[384,1449,1450],{"class":396},"in",[384,1452,1453],{"class":407}," $(",[384,1455,858],{"class":572},[384,1457,1247],{"class":787},[384,1459,1217],{"class":403},[384,1461,1220],{"class":396},[384,1463,876],{"class":572},[384,1465,1256],{"class":787},[384,1467,1468],{"class":403}," 'assets\u002F[^\"]+\\.(js|css)'",[384,1470,1471],{"class":407},"); ",[384,1473,1474],{"class":396},"do\n",[384,1476,1477,1480,1482,1484,1486,1488,1490,1492,1494,1497,1499,1502,1504],{"class":115,"line":457},[384,1478,1479],{"class":407},"  code",[384,1481,1239],{"class":396},[384,1483,1242],{"class":407},[384,1485,858],{"class":572},[384,1487,1247],{"class":787},[384,1489,1305],{"class":787},[384,1491,1308],{"class":403},[384,1493,1311],{"class":787},[384,1495,1496],{"class":403}," '%{http_code}'",[384,1498,1279],{"class":403},[384,1500,1501],{"class":407},"$a",[384,1503,1285],{"class":403},[384,1505,1270],{"class":407},[384,1507,1508,1511,1513,1516,1518,1520,1523,1526,1528,1531,1533,1536,1538,1541,1543,1545,1547,1549,1551],{"class":115,"line":470},[384,1509,1510],{"class":407},"  [ ",[384,1512,1285],{"class":403},[384,1514,1515],{"class":407},"$code",[384,1517,1285],{"class":403},[384,1519,400],{"class":396},[384,1521,1522],{"class":403}," \"200\"",[384,1524,1525],{"class":407}," ] ",[384,1527,699],{"class":396},[384,1529,1530],{"class":407}," { ",[384,1532,1402],{"class":787},[384,1534,1535],{"class":403}," \"FAIL: ",[384,1537,1501],{"class":407},[384,1539,1540],{"class":403}," -> ",[384,1542,1515],{"class":407},[384,1544,1285],{"class":403},[384,1546,1431],{"class":407},[384,1548,1434],{"class":787},[384,1550,1437],{"class":787},[384,1552,420],{"class":407},[384,1554,1555],{"class":115,"line":484},[384,1556,1557],{"class":396},"done\n",[384,1559,1560],{"class":115,"line":490},[384,1561,1562],{"class":389},"# trade-off: this greps the served HTML, so it can't see hashes behind a\n",[384,1564,1565],{"class":115,"line":496},[384,1566,1567],{"class":389},"# client Service Worker. Pair it with a synthetic returning-user check when\n",[384,1569,1570],{"class":115,"line":502},[384,1571,1572],{"class":389},"# a SW is in play; skip that only for sites with no Service Worker at all.\n",[14,1574,1575,1576,1578,1579,1581,1582,1585,1586,1588,1589,1591,1592,1594,1595,1599],{},"The passing state: the document returns ",[29,1577,1050],{}," (or a ",[29,1580,119],{}," on revalidation), every hash it references returns ",[29,1583,1584],{},"200",", and assets carry ",[29,1587,229],{},". In CI, run this gate alongside your tag-purge checks. In the field, watch RUM for a flat-line of zero ",[29,1590,270],{}," \u002F asset-404 beacons across the deploy boundary, and confirm document ",[29,1593,261],{}," resets near zero on the edge immediately after release. Soft-purging the HTML on deploy, paired with a short ",[18,1596,1598],{"href":1597},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002F","stale-while-revalidate"," window, smooths the transition further.",[182,1601,1603],{"id":1602},"related","Related",[190,1605,1606,1611,1617,1624,1629],{},[193,1607,1608,1610],{},[18,1609,21],{"href":20}," — where invalidation-by-omission fits among the four purge primitives.",[193,1612,1613,1616],{},[18,1614,1615],{"href":517},"Setting up immutable cache headers for hashed assets"," — the header config that makes omission safe.",[193,1618,1619,1623],{},[18,1620,1622],{"href":1621},"\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,1625,1626,1628],{},[18,1627,743],{"href":742}," — keeping the SW from shadowing a fresh index document.",[193,1630,1631,1635],{},[18,1632,1634],{"href":1633},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002F","CDN Edge Caching Configuration"," — edge TTLs and cache keys for the HTML entry point.",[1637,1638,1640],"script",{"type":1639},"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",[1642,1643,1644],"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":1646},[1647,1648,1654,1660,1661],{"id":184,"depth":393,"text":185},{"id":295,"depth":393,"text":296,"children":1649},[1650,1651,1652,1653],{"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":1655},[1656,1657,1658,1659],{"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":1083,"depth":393,"text":1084},{"id":1602,"depth":393,"text":1603},"How to ship new immutable hashed bundles without a stale index document pointing at old or missing hashes.","md",{"slug":12,"type":1665,"breadcrumb":1666,"datePublished":1673,"dateModified":1673},"article",[1667,1669,1670,1671],{"name":1668,"url":206},"Home",{"name":26,"url":25},{"name":21,"url":20},{"name":5,"url":1672},"\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":1676,"description":1677},"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","avlThqM7I0ZeRnVoR0KBCBPEtrMSW2crUyMWp1bhu1M",[1681,1684],{"title":21,"path":1682,"stem":1683},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Findex",{"title":1622,"path":1685,"stem":1686},"\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",1784398534545]