[{"data":1,"prerenderedAt":1074},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy":1066},{"id":4,"title":5,"body":6,"description":1046,"extension":1047,"meta":1048,"navigation":1060,"path":1061,"seo":1062,"stem":1064,"__hash__":1065},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002Findex.md","Fixing Stale HTML After a Deploy",{"type":7,"value":8,"toc":1038},"minimark",[9,14,38,46,51,100,256,260,277,283,297,303,307,312,420,423,429,510,513,519,623,785,796,800,811,814,919,922,926,950,959,971,980,989,998,1002,1023,1028,1031,1034],[10,11,13],"h1",{"id":12},"how-to-fix-stale-html-served-after-a-deploy","How to Fix Stale HTML Served After a Deploy",[15,16,17,18,23,24,28,29,33,34,37],"p",{},"This walkthrough sits under ",[19,20,22],"a",{"href":21},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F","Cache Invalidation Patterns"," in ",[19,25,27],{"href":26},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",". Ten minutes after a deploy, support reports a blank page. The cause is almost always the same: a cached HTML document from the previous build is still being served, and it references ",[30,31,32],"code",{},"app.9f3c2a.js"," — a file that no longer exists, because the new build emitted ",[30,35,36],{},"app.7b1e88.js"," and the old chunk was deleted.",[15,39,40,41,45],{},"The asset caching is doing exactly what you asked. The mistake is caching the ",[42,43,44],"em",{},"document"," the same way you cache the assets it points at.",[47,48,50],"h2",{"id":49},"rapid-diagnosis-locate-the-layer-holding-the-old-document","Rapid Diagnosis: Locate the Layer Holding the Old Document",[52,53,54,74,88,94],"ul",{},[55,56,57,61,62,65,66,69,70,73],"li",{},[58,59,60],"strong",{},"Check the response headers on the HTML."," ",[30,63,64],{},"curl -sI"," the page and read ",[30,67,68],{},"Cache-Control"," and any CDN age header. A long ",[30,71,72],{},"max-age"," on a document is the bug in one line.",[55,75,76,79,80,83,84,87],{},[58,77,78],{},"Look for 404s on hashed assets."," In the browser console, failures on ",[30,81,82],{},".js"," or ",[30,85,86],{},".css"," files with hashes that do not match the current build confirm the diagnosis.",[55,89,90,93],{},[58,91,92],{},"Distinguish edge from browser."," A hard refresh that fixes it means the browser cache; a hard refresh that does not means the CDN — or a service worker.",[55,95,96,99],{},[58,97,98],{},"Check the service worker."," In Application → Service Workers, a worker in \"waiting\" state is serving the old precached document until every tab closes.",[101,102,109,110,61,117,61,121,61,125,61,134,61,141,61,150,61,157,61,162,61,169,61,174,61,180,61,184,61,187,61,191,61,194,61,197,61,201,61,204,61,208,61,211,61,215,61,219,61,222,61,227,61,232,61,238,61,244,61,247,61,251,61],"svg",{"xmlns":103,"viewBox":104,"width":105,"role":106,"ariaLabel":107,"style":108},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 264","100%","img","Three caching layers between the origin and the user — CDN, browser cache and service worker — each able to hold an old HTML document that references deleted asset hashes","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)","\n  ",[111,112],"rect",{"className":113,"x":115,"y":115,"width":105,"height":105,"fill":116},[114],"svg-canvas","0","#ffffff",[118,119,120],"title",{},"Three places the old document can be hiding",[122,123,124],"desc",{},"The new build sits at the origin while the CDN, the browser HTTP cache and the service worker may each still hold the previous document referencing deleted hashed assets.",[111,126],{"x":127,"y":127,"width":128,"height":129,"rx":130,"fill":131,"stroke":132,"style":133},"1","758","262","10","none","currentColor","stroke-opacity:0.18",[135,136,140],"text",{"x":137,"y":138,"fill":132,"style":139},"24","32","font-size:16px;font-weight:700","Who is still serving the old index.html?",[111,142],{"x":143,"y":144,"width":145,"height":146,"rx":147,"fill":148,"stroke":148,"style":149},"30","70","140","56","7","#0466c8","fill-opacity:0.18;stroke-opacity:0.55",[135,151,156],{"x":152,"y":153,"fill":132,"style":154,"textAnchor":155},"100","94","font-size:12px;font-weight:700;","middle","Origin",[135,158,161],{"x":152,"y":159,"fill":132,"style":160,"textAnchor":155},"112","font-size:10.5px;","new build ✓",[163,164],"line",{"x1":165,"y1":166,"x2":167,"y2":166,"stroke":132,"style":168},"170","98","196","stroke-opacity:0.4",[170,171],"polygon",{"points":172,"fill":132,"style":173},"204,98 194,93 194,103","fill-opacity:0.55",[111,175],{"x":176,"y":144,"width":145,"height":146,"rx":147,"fill":177,"stroke":178,"style":179},"204","#ffc300","#b8860b","fill-opacity:0.28",[135,181,183],{"x":182,"y":153,"fill":132,"style":154,"textAnchor":155},"274","CDN edge",[135,185,186],{"x":182,"y":159,"fill":132,"style":160,"textAnchor":155},"old doc, TTL 1h",[163,188],{"x1":189,"y1":166,"x2":190,"y2":166,"stroke":132,"style":168},"344","370",[170,192],{"points":193,"fill":132,"style":173},"378,98 368,93 368,103",[111,195],{"x":196,"y":144,"width":145,"height":146,"rx":147,"fill":177,"stroke":178,"style":179},"378",[135,198,200],{"x":199,"y":153,"fill":132,"style":154,"textAnchor":155},"448","Browser cache",[135,202,203],{"x":199,"y":159,"fill":132,"style":160,"textAnchor":155},"old doc, max-age",[163,205],{"x1":206,"y1":166,"x2":207,"y2":166,"stroke":132,"style":168},"518","544",[170,209],{"points":210,"fill":132,"style":173},"552,98 542,93 542,103",[111,212],{"x":213,"y":144,"width":214,"height":146,"rx":147,"fill":177,"stroke":178,"style":179},"552","176",[135,216,218],{"x":217,"y":153,"fill":132,"style":154,"textAnchor":155},"640","Service worker",[135,220,221],{"x":217,"y":159,"fill":132,"style":160,"textAnchor":155},"precached old doc",[163,223],{"x1":137,"y1":224,"x2":225,"y2":224,"stroke":132,"style":226},"150","736","stroke-opacity:0.2",[135,228,231],{"x":137,"y":229,"fill":132,"style":230},"178","font-size:11.5px;font-weight:700","The document must revalidate; the hashed assets must not.",[111,233],{"x":143,"y":234,"width":235,"height":143,"rx":236,"fill":132,"stroke":132,"style":237},"192","330","5","fill-opacity:0.05;stroke-opacity:0.3",[135,239,243],{"x":240,"y":241,"fill":132,"style":242,"textAnchor":155},"195","212","font-size:11px;","HTML: no-cache — always revalidate",[111,245],{"x":246,"y":234,"width":235,"height":143,"rx":236,"fill":132,"stroke":132,"style":237},"400",[135,248,250],{"x":249,"y":241,"fill":132,"style":242,"textAnchor":155},"565","\u002Fassets\u002F*: max-age=31536000, immutable",[135,252,255],{"x":137,"y":253,"fill":132,"style":254},"246","font-size:11px;font-style:italic;fill-opacity:0.7","Same deploy, two opposite policies — that asymmetry is the whole fix.",[47,257,259],{"id":258},"root-cause-analysis","Root Cause Analysis",[15,261,262,268,269,272,273,276],{},[58,263,264,265,267],{},"1. HTML cached with a long ",[30,266,72],{},"."," The document is the index of your build; it must be revalidated on every load or it will outlive the files it names. ",[30,270,271],{},"no-cache"," (revalidate, but reuse if unchanged) is almost always the right policy — not ",[30,274,275],{},"no-store",", which forfeits the efficiency of a 304.",[15,278,279,282],{},[58,280,281],{},"2. Old assets deleted at deploy time."," If the previous build's chunks are removed immediately, any surviving old document breaks instantly. Keeping the last two builds' assets around costs very little storage and converts a hard failure into a soft one.",[15,284,285,288,289,292,293,296],{},[58,286,287],{},"3. Service worker serving a precached document."," A precached ",[30,290,291],{},"index.html"," is served by the ",[42,294,295],{},"old"," worker until the new one activates, which by default waits for every tab to close. Users with long-lived tabs can stay on an old build for days.",[15,298,299,302],{},[58,300,301],{},"4. Purge order inverted."," Purging the HTML before the new assets are fully propagated means a user can fetch the new document and then 404 on assets that have not reached their edge yet.",[47,304,306],{"id":305},"step-by-step-resolution","Step-by-Step Resolution",[15,308,309],{},[58,310,311],{},"1. Split the cache policy by content type.",[313,314,319],"pre",{"className":315,"code":316,"language":317,"meta":318,"style":318},"language-nginx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# The document: always revalidate, but allow a cheap 304.\nlocation = \u002Findex.html {\n  add_header Cache-Control \"no-cache\" always;\n}\n# Content-hashed assets: never revalidate, they are immutable by construction.\nlocation \u002Fassets\u002F {\n  add_header Cache-Control \"public, max-age=31536000, immutable\" always;\n}\n# trade-off: no-cache still costs a conditional request per navigation. On a\n# high-traffic origin, put a short edge TTL with stale-while-revalidate in\n# front so the revalidation happens at the edge, not at the origin.\n","nginx","",[30,320,321,329,347,362,368,374,385,397,402,408,414],{"__ignoreMap":318},[322,323,325],"span",{"class":163,"line":324},1,[322,326,328],{"class":327},"sjfSM","# The document: always revalidate, but allow a cheap 304.\n",[322,330,332,336,339,343],{"class":163,"line":331},2,[322,333,335],{"class":334},"sPARh","location",[322,337,338],{"class":334}," =",[322,340,342],{"class":341},"sZ8jY"," \u002Findex.html ",[322,344,346],{"class":345},"saISM","{\n",[322,348,350,353,356,359],{"class":163,"line":349},3,[322,351,352],{"class":334},"  add_header ",[322,354,355],{"class":345},"Cache-Control ",[322,357,358],{"class":341},"\"no-cache\"",[322,360,361],{"class":345}," always;\n",[322,363,365],{"class":163,"line":364},4,[322,366,367],{"class":345},"}\n",[322,369,371],{"class":163,"line":370},5,[322,372,373],{"class":327},"# Content-hashed assets: never revalidate, they are immutable by construction.\n",[322,375,377,379,383],{"class":163,"line":376},6,[322,378,335],{"class":334},[322,380,382],{"class":381},"sQw3B"," \u002Fassets\u002F ",[322,384,346],{"class":345},[322,386,388,390,392,395],{"class":163,"line":387},7,[322,389,352],{"class":334},[322,391,355],{"class":345},[322,393,394],{"class":341},"\"public, max-age=31536000, immutable\"",[322,396,361],{"class":345},[322,398,400],{"class":163,"line":399},8,[322,401,367],{"class":345},[322,403,405],{"class":163,"line":404},9,[322,406,407],{"class":327},"# trade-off: no-cache still costs a conditional request per navigation. On a\n",[322,409,411],{"class":163,"line":410},10,[322,412,413],{"class":327},"# high-traffic origin, put a short edge TTL with stale-while-revalidate in\n",[322,415,417],{"class":163,"line":416},11,[322,418,419],{"class":327},"# front so the revalidation happens at the edge, not at the origin.\n",[15,421,422],{},"Expected outcome: no browser or edge can serve a document older than the current build without checking.",[15,424,425,428],{},[58,426,427],{},"2. Keep the previous build's assets."," Deploy additively and prune on a delay.",[313,430,434],{"className":431,"code":432,"language":433,"meta":318,"style":318},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# Sync new assets WITHOUT deleting the old ones, then prune builds older than 7 days.\naws s3 sync .\u002Fdist\u002Fassets s3:\u002F\u002Fcdn-bucket\u002Fassets --cache-control \\\n  \"public, max-age=31536000, immutable\"\naws s3 sync .\u002Fdist s3:\u002F\u002Fcdn-bucket --exclude \"assets\u002F*\" --cache-control \"no-cache\"\n# trade-off: retaining old assets costs storage and can hide a genuine\n# regression, because users on stale documents keep working silently. Keep the\n# retention short and monitor how many requests still hit old hashes.\n","bash",[30,435,436,441,465,470,495,500,505],{"__ignoreMap":318},[322,437,438],{"class":163,"line":324},[322,439,440],{"class":327},"# Sync new assets WITHOUT deleting the old ones, then prune builds older than 7 days.\n",[322,442,443,446,449,452,455,458,462],{"class":163,"line":331},[322,444,445],{"class":381},"aws",[322,447,448],{"class":341}," s3",[322,450,451],{"class":341}," sync",[322,453,454],{"class":341}," .\u002Fdist\u002Fassets",[322,456,457],{"class":341}," s3:\u002F\u002Fcdn-bucket\u002Fassets",[322,459,461],{"class":460},"sPXB4"," --cache-control",[322,463,464],{"class":334}," \\\n",[322,466,467],{"class":163,"line":349},[322,468,469],{"class":341},"  \"public, max-age=31536000, immutable\"\n",[322,471,472,474,476,478,481,484,487,490,492],{"class":163,"line":364},[322,473,445],{"class":381},[322,475,448],{"class":341},[322,477,451],{"class":341},[322,479,480],{"class":341}," .\u002Fdist",[322,482,483],{"class":341}," s3:\u002F\u002Fcdn-bucket",[322,485,486],{"class":460}," --exclude",[322,488,489],{"class":341}," \"assets\u002F*\"",[322,491,461],{"class":460},[322,493,494],{"class":341}," \"no-cache\"\n",[322,496,497],{"class":163,"line":370},[322,498,499],{"class":327},"# trade-off: retaining old assets costs storage and can hide a genuine\n",[322,501,502],{"class":163,"line":376},[322,503,504],{"class":327},"# regression, because users on stale documents keep working silently. Keep the\n",[322,506,507],{"class":163,"line":387},[322,508,509],{"class":327},"# retention short and monitor how many requests still hit old hashes.\n",[15,511,512],{},"Expected outcome: a stale document degrades to \"slightly old version\" instead of \"broken page\".",[15,514,515,518],{},[58,516,517],{},"3. Fix the service-worker update path."," Take control promptly and prompt the user rather than silently pinning them to an old build.",[313,520,524],{"className":521,"code":522,"language":523,"meta":318,"style":318},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F In the worker: activate immediately once the user has agreed.\nself.addEventListener('message', (e) => {\n  if (e.data?.type === 'SKIP_WAITING') self.skipWaiting();\n});\nself.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));\n","javascript",[30,525,526,531,561,584,589],{"__ignoreMap":318},[322,527,528],{"class":163,"line":324},[322,529,530],{"class":327},"\u002F\u002F In the worker: activate immediately once the user has agreed.\n",[322,532,533,536,540,543,546,549,552,555,558],{"class":163,"line":331},[322,534,535],{"class":345},"self.",[322,537,539],{"class":538},"smZ65","addEventListener",[322,541,542],{"class":345},"(",[322,544,545],{"class":341},"'message'",[322,547,548],{"class":345},", (",[322,550,551],{"class":381},"e",[322,553,554],{"class":345},") ",[322,556,557],{"class":334},"=>",[322,559,560],{"class":345}," {\n",[322,562,563,566,569,572,575,578,581],{"class":163,"line":349},[322,564,565],{"class":334},"  if",[322,567,568],{"class":345}," (e.data?.type ",[322,570,571],{"class":334},"===",[322,573,574],{"class":341}," 'SKIP_WAITING'",[322,576,577],{"class":345},") self.",[322,579,580],{"class":538},"skipWaiting",[322,582,583],{"class":345},"();\n",[322,585,586],{"class":163,"line":364},[322,587,588],{"class":345},"});\n",[322,590,591,593,595,597,600,602,604,606,608,611,614,617,620],{"class":163,"line":370},[322,592,535],{"class":345},[322,594,539],{"class":538},[322,596,542],{"class":345},[322,598,599],{"class":341},"'activate'",[322,601,548],{"class":345},[322,603,551],{"class":381},[322,605,554],{"class":345},[322,607,557],{"class":334},[322,609,610],{"class":345}," e.",[322,612,613],{"class":538},"waitUntil",[322,615,616],{"class":345},"(self.clients.",[322,618,619],{"class":538},"claim",[322,621,622],{"class":345},"()));\n",[313,624,626],{"className":521,"code":625,"language":523,"meta":318,"style":318},"\u002F\u002F In the page: detect the waiting worker and offer a reload.\nconst reg = await navigator.serviceWorker.getRegistration();\nreg?.addEventListener('updatefound', () => {\n  const sw = reg.installing;\n  sw?.addEventListener('statechange', () => {\n    if (sw.state === 'installed' && navigator.serviceWorker.controller) {\n      showUpdateToast(() => { sw.postMessage({ type: 'SKIP_WAITING' }); location.reload(); });\n    }\n  });\n});\n\u002F\u002F trade-off: skipWaiting swaps the worker under open tabs, so a page mid-task\n\u002F\u002F can end up with mismatched code and data. Prompting the user first is the\n\u002F\u002F safe compromise; forcing it silently is not.\n",[30,627,628,633,654,673,686,704,723,754,759,764,768,773,779],{"__ignoreMap":318},[322,629,630],{"class":163,"line":324},[322,631,632],{"class":327},"\u002F\u002F In the page: detect the waiting worker and offer a reload.\n",[322,634,635,638,641,643,646,649,652],{"class":163,"line":331},[322,636,637],{"class":334},"const",[322,639,640],{"class":460}," reg",[322,642,338],{"class":334},[322,644,645],{"class":334}," await",[322,647,648],{"class":345}," navigator.serviceWorker.",[322,650,651],{"class":538},"getRegistration",[322,653,583],{"class":345},[322,655,656,659,661,663,666,669,671],{"class":163,"line":349},[322,657,658],{"class":345},"reg?.",[322,660,539],{"class":538},[322,662,542],{"class":345},[322,664,665],{"class":341},"'updatefound'",[322,667,668],{"class":345},", () ",[322,670,557],{"class":334},[322,672,560],{"class":345},[322,674,675,678,681,683],{"class":163,"line":364},[322,676,677],{"class":334},"  const",[322,679,680],{"class":460}," sw",[322,682,338],{"class":334},[322,684,685],{"class":345}," reg.installing;\n",[322,687,688,691,693,695,698,700,702],{"class":163,"line":370},[322,689,690],{"class":345},"  sw?.",[322,692,539],{"class":538},[322,694,542],{"class":345},[322,696,697],{"class":341},"'statechange'",[322,699,668],{"class":345},[322,701,557],{"class":334},[322,703,560],{"class":345},[322,705,706,709,712,714,717,720],{"class":163,"line":376},[322,707,708],{"class":334},"    if",[322,710,711],{"class":345}," (sw.state ",[322,713,571],{"class":334},[322,715,716],{"class":341}," 'installed'",[322,718,719],{"class":334}," &&",[322,721,722],{"class":345}," navigator.serviceWorker.controller) {\n",[322,724,725,728,731,733,736,739,742,745,748,751],{"class":163,"line":387},[322,726,727],{"class":538},"      showUpdateToast",[322,729,730],{"class":345},"(() ",[322,732,557],{"class":334},[322,734,735],{"class":345}," { sw.",[322,737,738],{"class":538},"postMessage",[322,740,741],{"class":345},"({ type: ",[322,743,744],{"class":341},"'SKIP_WAITING'",[322,746,747],{"class":345}," }); location.",[322,749,750],{"class":538},"reload",[322,752,753],{"class":345},"(); });\n",[322,755,756],{"class":163,"line":399},[322,757,758],{"class":345},"    }\n",[322,760,761],{"class":163,"line":404},[322,762,763],{"class":345},"  });\n",[322,765,766],{"class":163,"line":410},[322,767,588],{"class":345},[322,769,770],{"class":163,"line":416},[322,771,772],{"class":327},"\u002F\u002F trade-off: skipWaiting swaps the worker under open tabs, so a page mid-task\n",[322,774,776],{"class":163,"line":775},12,[322,777,778],{"class":327},"\u002F\u002F can end up with mismatched code and data. Prompting the user first is the\n",[322,780,782],{"class":163,"line":781},13,[322,783,784],{"class":327},"\u002F\u002F safe compromise; forcing it silently is not.\n",[15,786,787,790,791,795],{},[58,788,789],{},"4. Purge in the right order."," Deploy and propagate assets first, then purge the HTML — the sequence matters more than the mechanism, and the tag-based approach in ",[19,792,794],{"href":793},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F","purging CDN cache by tag on deploy"," makes it repeatable.",[47,797,799],{"id":798},"verification","Verification",[15,801,802,803,806,807,810],{},"Immediately after a deploy, confirm three responses: the HTML returns ",[30,804,805],{},"Cache-Control: no-cache"," and the new asset hashes, the hashed assets return ",[30,808,809],{},"immutable",", and a request for a previous build's asset still succeeds. That last check is the one people skip and the one that turns a bad deploy into an incident.",[15,812,813],{},"Add a client-side canary so a stale document reports itself instead of silently failing:",[313,815,817],{"className":521,"code":816,"language":523,"meta":318,"style":318},"\u002F\u002F The build stamps its own id into the HTML; the app checks it against \u002Fversion.json.\nconst res = await fetch('\u002Fversion.json', { cache: 'no-store' });\nconst { build } = await res.json();\nif (build !== document.documentElement.dataset.build) {\n  reportStaleDocument(document.documentElement.dataset.build, build);\n  \u002F\u002F Optionally reload once, guarded so it can never loop.\n}\n\u002F\u002F trade-off: an automatic reload risks interrupting a user mid-form. Report\n\u002F\u002F first, reload only on a navigation boundary or with the user's consent.\n",[30,818,819,824,852,878,892,900,905,909,914],{"__ignoreMap":318},[322,820,821],{"class":163,"line":324},[322,822,823],{"class":327},"\u002F\u002F The build stamps its own id into the HTML; the app checks it against \u002Fversion.json.\n",[322,825,826,828,831,833,835,838,840,843,846,849],{"class":163,"line":331},[322,827,637],{"class":334},[322,829,830],{"class":460}," res",[322,832,338],{"class":334},[322,834,645],{"class":334},[322,836,837],{"class":538}," fetch",[322,839,542],{"class":345},[322,841,842],{"class":341},"'\u002Fversion.json'",[322,844,845],{"class":345},", { cache: ",[322,847,848],{"class":341},"'no-store'",[322,850,851],{"class":345}," });\n",[322,853,854,856,859,862,865,868,870,873,876],{"class":163,"line":349},[322,855,637],{"class":334},[322,857,858],{"class":345}," { ",[322,860,861],{"class":460},"build",[322,863,864],{"class":345}," } ",[322,866,867],{"class":334},"=",[322,869,645],{"class":334},[322,871,872],{"class":345}," res.",[322,874,875],{"class":538},"json",[322,877,583],{"class":345},[322,879,880,883,886,889],{"class":163,"line":364},[322,881,882],{"class":334},"if",[322,884,885],{"class":345}," (build ",[322,887,888],{"class":334},"!==",[322,890,891],{"class":345}," document.documentElement.dataset.build) {\n",[322,893,894,897],{"class":163,"line":370},[322,895,896],{"class":538},"  reportStaleDocument",[322,898,899],{"class":345},"(document.documentElement.dataset.build, build);\n",[322,901,902],{"class":163,"line":376},[322,903,904],{"class":327},"  \u002F\u002F Optionally reload once, guarded so it can never loop.\n",[322,906,907],{"class":163,"line":387},[322,908,367],{"class":345},[322,910,911],{"class":163,"line":399},[322,912,913],{"class":327},"\u002F\u002F trade-off: an automatic reload risks interrupting a user mid-form. Report\n",[322,915,916],{"class":163,"line":404},[322,917,918],{"class":327},"\u002F\u002F first, reload only on a navigation boundary or with the user's consent.\n",[15,920,921],{},"Then watch for chunk-load errors in your error tracker. A spike immediately after each deploy that decays over an hour is the unmistakable signature of a document TTL that is still too long somewhere in the chain.",[47,923,925],{"id":924},"faq","FAQ",[927,928,931,935],"details",{"className":929},[930],"faq-item",[932,933,934],"summary",{},"Should HTML use no-cache or no-store?",[15,936,937,939,940,943,944,946,947,949],{},[30,938,271],{},", in almost every case. It requires revalidation but still allows a ",[30,941,942],{},"304 Not Modified",", so an unchanged document costs headers rather than a full body. ",[30,945,275],{}," forbids storage entirely, forcing a complete re-download on every navigation including back\u002Fforward — a real cost for no additional correctness. Reserve ",[30,948,275],{}," for responses that must never be written to disk, such as personal data on a shared device.",[927,951,953,956],{"className":952},[930],[932,954,955],{},"How long should I keep the previous build's assets?",[15,957,958],{},"Long enough to cover the tail of stale documents in the wild — usually a few days, and at least as long as your longest HTML TTL plus your service-worker update window. Measure it: log requests for asset hashes that are not in the current build and see how long they persist. If they never fully stop, something is pinning users to old documents and retention is masking it.",[927,960,962,965],{"className":961},[930],[932,963,964],{},"Why do users still see the old page after I purged the CDN?",[15,966,967,968,970],{},"Because the CDN is only one of three layers. The browser's own HTTP cache will keep serving a document with a long ",[30,969,72],{}," regardless of what the edge does, and a service worker can serve a precached document without touching the network at all. Fix the policy at every layer; purging only clears the one you control most directly.",[927,972,974,977],{"className":973},[930],[932,975,976],{},"Should the app reload itself when it detects a stale document?",[15,978,979],{},"Only at a safe moment. An automatic reload in the middle of a form or a checkout destroys work the user has done, and a reload loop caused by a mismatch that never resolves is worse than the original bug. The pattern that holds up is to detect, report, and then either wait for the next in-app navigation or surface a small \"a new version is available\" affordance. Guard any automatic path with a one-shot flag so it can never repeat.",[927,981,983,986],{"className":982},[930],[932,984,985],{},"How does this interact with a static site host that manages caching for me?",[15,987,988],{},"Most managed hosts apply the correct split by default — revalidate HTML, immutable hashed assets — but they also let a framework or a config file override it, which is where the bug usually enters. Verify the actual response headers after a deploy rather than trusting the platform's documentation, and check both the apex document and a nested route, since some hosts treat them differently.",[927,990,992,995],{"className":991},[930],[932,993,994],{},"Is there a way to make chunk-load failures recover automatically?",[15,996,997],{},"Yes, and it is worth having as a safety net. Wrap dynamic imports so that a load failure triggers one retry against a fresh document — reload the page once, with a marker in session storage so a second failure surfaces an error instead of looping. It does not fix the underlying caching problem, but it converts a blank screen into a brief flicker while you deploy the real fix.",[47,999,1001],{"id":1000},"related","Related",[52,1003,1004,1009,1016],{},[55,1005,1006,1008],{},[19,1007,22],{"href":21}," — the invalidation strategies this failure sits inside.",[55,1010,1011,1015],{},[19,1012,1014],{"href":1013},"\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"," — the other half of the split policy.",[55,1017,1018,1022],{},[19,1019,1021],{"href":1020},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Fprecaching-vs-runtime-caching-with-workbox\u002F","Precaching vs runtime caching with Workbox"," — why a precached document needs an explicit update path.",[1024,1025,1027],"script",{"type":1026},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Fix stale HTML served after a deploy\",\n  \"description\": \"Split cache policy so documents revalidate while hashed assets stay immutable, retain the previous build's assets, correct the service-worker update path, and purge in the right order.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"position\": 1, \"name\": \"Split the cache policy\", \"text\": \"Serve HTML with no-cache and content-hashed assets with a year-long immutable policy.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 2, \"name\": \"Retain previous assets\", \"text\": \"Deploy additively and prune old builds on a delay so a stale document still resolves its chunks.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 3, \"name\": \"Fix the worker update path\", \"text\": \"Detect the waiting service worker, prompt the user, then skip waiting and claim clients.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 4, \"name\": \"Purge in order and verify\", \"text\": \"Propagate assets before purging HTML, then confirm headers, hashes and old-asset availability after deploy.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002F#verification\" }\n  ]\n}\n",[1024,1029,1030],{"type":1026},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Fix Stale HTML Served After a Deploy\",\n  \"description\": \"The post-deploy blank page: a cached document referencing deleted hashed assets, and the layered cache fixes that prevent it.\",\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\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002F\" }\n}\n",[1024,1032,1033],{"type":1026},"\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\": \"Cache Invalidation Patterns\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Fixing Stale HTML After a Deploy\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002F\" }\n  ]\n}\n",[1035,1036,1037],"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 .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}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 .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}",{"title":318,"searchDepth":331,"depth":331,"links":1039},[1040,1041,1042,1043,1044,1045],{"id":49,"depth":331,"text":50},{"id":258,"depth":331,"text":259},{"id":305,"depth":331,"text":306},{"id":798,"depth":331,"text":799},{"id":924,"depth":331,"text":925},{"id":1000,"depth":331,"text":1001},"The classic post-deploy failure: cached HTML pointing at chunks that no longer exist. Correct the caching layers in the right order and add a client-side recovery path.","md",{"slug":1049,"type":1050,"breadcrumb":1051,"datePublished":1059,"dateModified":1059},"fixing-stale-html-after-a-deploy","article",[1052,1055,1056,1057],{"name":1053,"url":1054},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1058},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002F","2026-08-01",true,"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy",{"title":5,"description":1063},"Users load an old document that references deleted hashed assets and the page breaks. Fix the HTML cache policy, the service worker update path, and the CDN purge order.","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Ffixing-stale-html-after-a-deploy\u002Findex","SDXTg016cMcUxRuTD3g0cWcnDA9gcWLOM0w4qDjkCn4",[1067,1070],{"title":22,"path":1068,"stem":1069,"children":-1},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Findex",{"title":1071,"path":1072,"stem":1073,"children":-1},"Invalidating immutable hashed assets safely","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002Findex",1785622407090]