[{"data":1,"prerenderedAt":1376},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F":1368},{"id":4,"title":5,"body":6,"description":1351,"extension":1352,"meta":1353,"navigation":1089,"path":1362,"seo":1363,"stem":1366,"__hash__":1367},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Findex.md","Cache Invalidation Patterns",{"type":7,"value":8,"toc":1340},"minimark",[9,13,23,47,176,181,184,264,267,271,291,392,410,414,417,473,486,490,493,587,598,602,617,691,694,701,715,719,722,752,755,759,768,998,1001,1021,1041,1045,1048,1278,1290,1294,1331,1336],[10,11,5],"h1",{"id":12},"cache-invalidation-patterns",[14,15,16,17,22],"p",{},"This guide sits under ",[18,19,21],"a",{"href":20},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture"," and covers the hardest problem in caching: deciding what to evict, when, and how — without serving stale bytes or stampeding your origin.",[14,24,25,26,30,31,34,35,38,39,42,43,46],{},"Cache invalidation is a two-sided optimization. Push too little and users see stale content: a deployed bugfix that never reaches the edge, a price that changed an hour ago, a ",[27,28,29],"code",{},"index.html"," that still references a deleted JS bundle. Push too much and you trigger thundering-herd revalidation — a single broad purge drops thousands of edge objects simultaneously, every subsequent request misses, and the origin absorbs a synchronized traffic spike that blows past its TTFB budget. The actionable boundary is concrete: cached static assets should hold a ",[27,32,33],{},">85%"," edge hit ratio, cached TTFB should stay ",[27,36,37],{},"≤50ms",", and a deploy purge should propagate across PoPs in ",[27,40,41],{},"\u003C2s"," while keeping the post-purge origin request rate inside the headroom your origin can serve under its ",[27,44,45],{},"TTFB ≤ 200ms"," target. This guide moves from baseline capture to root-cause isolation to a targeted purge strategy and CI validation.",[14,48,49],{},[50,51,58,59,58,63,58,67,58,77,58,84,58,91,58,97,58,102,58,105,58,109,58,112,58,118,58,122,58,125,58,132,58,137,58,143,58,148,58,152,58,155,58,159,58,162,58,167,58,172,58],"svg",{"xmlns":52,"viewBox":53,"width":54,"role":55,"ariaLabel":56,"style":57},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 320","100%","img","Tag-based purge flow from deploy through surrogate-key purge API to selective edge eviction","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:#001d3d"," ",[60,61,62],"title",{},"Tag-based purge flow",[64,65,66],"desc",{},"A deploy tags responses with surrogate keys; a purge API call evicts only objects carrying the affected key, leaving unrelated edge objects warm.",[68,69],"rect",{"x":70,"y":70,"width":71,"height":72,"rx":73,"fill":74,"stroke":75,"style":76},"1","758","318","10","none","currentColor","stroke-opacity:0.18",[78,79,83],"text",{"x":80,"y":81,"fill":75,"style":82},"24","38","font-size:18px;font-weight:700","Purge by tag, not by everything",[68,85],{"x":80,"y":86,"width":87,"height":86,"rx":88,"fill":89,"stroke":89,"style":90},"64","200","6","#0466c8","fill-opacity:0.14",[78,92,96],{"x":93,"y":94,"fill":75,"style":95},"124","92","font-size:13px;font-weight:600;text-anchor:middle","Origin response",[78,98,101],{"x":93,"y":99,"fill":75,"style":100},"112","font-size:12px;text-anchor:middle","Surrogate-Key: product-42",[68,103],{"x":104,"y":86,"width":87,"height":86,"rx":88,"fill":89,"stroke":89,"style":90},"280",[78,106,108],{"x":107,"y":94,"fill":75,"style":95},"380","Edge stores object",[78,110,111],{"x":107,"y":99,"fill":75,"style":100},"indexed by its keys",[68,113],{"x":114,"y":86,"width":87,"height":86,"rx":88,"fill":115,"stroke":116,"style":117},"536","#ffc300","#b8860b","fill-opacity:0.22",[78,119,121],{"x":120,"y":94,"fill":75,"style":95},"636","Deploy \u002F CMS event",[78,123,124],{"x":120,"y":99,"fill":75,"style":100},"POST purge product-42",[126,127],"line",{"x1":128,"y1":129,"x2":130,"y2":129,"stroke":75,"style":131},"224","96","278","stroke-opacity:0.4",[126,133],{"x1":114,"y1":134,"x2":107,"y2":135,"stroke":116,"style":136},"128","160","stroke-opacity:0.6",[68,138],{"x":80,"y":139,"width":140,"height":141,"rx":88,"fill":89,"stroke":89,"style":142},"172","340","60","stroke-opacity:0.5;fill-opacity:0.10",[78,144,147],{"x":145,"y":146,"fill":75,"style":95},"194","198","Tagged objects evicted",[78,149,151],{"x":145,"y":150,"fill":75,"style":100},"217","refetched on next hit",[68,153],{"x":154,"y":139,"width":140,"height":141,"rx":88,"fill":89,"stroke":89,"style":142},"396",[78,156,158],{"x":157,"y":146,"fill":75,"style":95},"566","Untagged objects stay warm",[78,160,161],{"x":157,"y":150,"fill":75,"style":100},"no origin stampede",[126,163],{"x1":80,"y1":164,"x2":165,"y2":164,"stroke":75,"style":166},"258","736","stroke-opacity:0.3",[78,168,171],{"x":80,"y":169,"fill":75,"style":170},"284","font-size:13px","Tag granularity sets each purge's blast radius",[78,173,175],{"x":80,"y":174,"fill":75,"style":170},"304","Soft purge + stale-while-revalidate absorbs refetch",[177,178,180],"h2",{"id":179},"prerequisites-versions-apis-and-tagging-capability","Prerequisites: Versions, APIs, and Tagging Capability",[14,182,183],{},"Before tuning invalidation, confirm your stack exposes the primitives this workflow depends on:",[185,186,187,222,229,251,261],"ul",{},[188,189,190,191,195,196,199,200,203,204,195,207,199,210,213,214,217,218,221],"li",{},"A CDN with tag-based purge: ",[192,193,194],"strong",{},"Fastly"," (",[27,197,198],{},"Surrogate-Key"," header + ",[27,201,202],{},"\u002Fservice\u002F\u003Cid>\u002Fpurge"," API), ",[192,205,206],{},"Cloudflare Enterprise",[27,208,209],{},"Cache-Tag",[27,211,212],{},"\u002Fpurge_cache"," with ",[27,215,216],{},"tags","), or ",[192,219,220],{},"CloudFront"," (path-pattern invalidations only — no native tagging, plan accordingly).",[188,223,224,225,228],{},"An API token scoped to ",[192,226,227],{},"purge only",", stored as a CI secret. Never reuse a full-access token in a deploy job.",[188,230,231,232,235,236,239,240,239,243,246,247,250],{},"A build that emits ",[192,233,234],{},"content-hashed filenames"," for JS\u002FCSS (Vite ",[27,237,238],{},"assetFileNames","\u002F",[27,241,242],{},"chunkFileNames",[27,244,245],{},"entryFileNames",", or Webpack ",[27,248,249],{},"[contenthash]","), so static assets are invalidated by omission rather than by purge.",[188,252,253,256,257,260],{},[27,254,255],{},"curl"," 7.x and ",[27,258,259],{},"jq"," for header inspection in CI.",[188,262,263],{},"For the client tier: a Service Worker (Workbox 7+ or hand-rolled) whose precache manifest is regenerated on every build.",[14,265,266],{},"With those in place, the remaining work is choosing the right purge primitive per asset class and wiring it into the deploy.",[177,268,270],{"id":269},"_1-environment-setup-tag-responses-with-surrogate-keys","1. Environment Setup: Tag Responses with Surrogate Keys",[14,272,273,274,278,279,282,283,286,287,290],{},"Invalidation granularity is decided at ",[275,276,277],"em",{},"response"," time, not purge time. The origin must stamp each cacheable response with the keys that identify the content inside it. A product page response might carry ",[27,280,281],{},"product-42",", ",[27,284,285],{},"category-shoes",", and ",[27,288,289],{},"layout-v3"," — purging any one of those keys later evicts this object.",[292,293,298],"pre",{"className":294,"code":295,"language":296,"meta":297,"style":297},"language-nginx shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","# Origin response for \u002Fproducts\u002F42 — emit surrogate keys at write time\nlocation ~ ^\u002Fproducts\u002F(\\d+)$ {\n  add_header Surrogate-Key \"product-$1 category-shoes layout-v3\";\n  add_header Cache-Control \"public, max-age=0, s-maxage=86400\";\n  # trade-off: max-age=0 keeps the browser revalidating while the edge\n  # holds it for a day. Do NOT use this for assets you cannot purge by tag —\n  # without tag purge you'd be stuck serving stale content for the full day.\n  proxy_pass http:\u002F\u002Fbackend;\n}\n","nginx","",[27,299,300,308,326,346,359,365,371,377,386],{"__ignoreMap":297},[301,302,304],"span",{"class":126,"line":303},1,[301,305,307],{"class":306},"sIIH1","# Origin response for \u002Fproducts\u002F42 — emit surrogate keys at write time\n",[301,309,311,315,318,322],{"class":126,"line":310},2,[301,312,314],{"class":313},"sP5qI","location",[301,316,317],{"class":313}," ~",[301,319,321],{"class":320},"s-_DF"," ^\u002Fproducts\u002F(\\d+)$ ",[301,323,325],{"class":324},"syybb","{\n",[301,327,329,332,335,338,340,343],{"class":126,"line":328},3,[301,330,331],{"class":313},"  add_header ",[301,333,334],{"class":324},"Surrogate-Key ",[301,336,337],{"class":320},"\"product-$",[301,339,70],{"class":324},[301,341,342],{"class":320}," category-shoes layout-v3\"",[301,344,345],{"class":324},";\n",[301,347,349,351,354,357],{"class":126,"line":348},4,[301,350,331],{"class":313},[301,352,353],{"class":324},"Cache-Control ",[301,355,356],{"class":320},"\"public, max-age=0, s-maxage=86400\"",[301,358,345],{"class":324},[301,360,362],{"class":126,"line":361},5,[301,363,364],{"class":306},"  # trade-off: max-age=0 keeps the browser revalidating while the edge\n",[301,366,368],{"class":126,"line":367},6,[301,369,370],{"class":306},"  # holds it for a day. Do NOT use this for assets you cannot purge by tag —\n",[301,372,374],{"class":126,"line":373},7,[301,375,376],{"class":306},"  # without tag purge you'd be stuck serving stale content for the full day.\n",[301,378,380,383],{"class":126,"line":379},8,[301,381,382],{"class":313},"  proxy_pass ",[301,384,385],{"class":324},"http:\u002F\u002Fbackend;\n",[301,387,389],{"class":126,"line":388},9,[301,390,391],{"class":324},"}\n",[14,393,394,395,397,398,400,401,404,405,409],{},"The header name is CDN-specific: Fastly reads ",[27,396,198],{},", Cloudflare reads ",[27,399,209],{},". Most edges strip the header before it reaches the browser, so it costs nothing on the wire. The discipline that matters: tag by ",[275,402,403],{},"content identity and dependency",", not by URL. A URL is one address; a tag can span hundreds of URLs that share a dependency, which is exactly what makes selective purge possible. Header placement and edge cache-key alignment are covered in depth in ",[18,406,408],{"href":407},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002F","CDN Edge Caching Configuration",".",[177,411,413],{"id":412},"_2-capture-baseline-measure-hit-ratio-and-post-purge-origin-load","2. Capture Baseline: Measure Hit Ratio and Post-Purge Origin Load",[14,415,416],{},"You cannot tell whether a purge strategy is over- or under-firing without numbers. Capture three baselines before changing anything.",[292,418,422],{"className":419,"code":420,"language":421,"meta":297,"style":297},"language-bash shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","# Baseline edge behaviour for a representative asset\ncurl -sI https:\u002F\u002Fyour-domain.com\u002Fproducts\u002F42 \\\n  | grep -Ei \"cache-control|age|x-cache|cf-cache-status|surrogate-key\"\n# trade-off: a one-shot curl shows steady-state freshness but NOT the\n# origin spike a broad purge causes — for that, watch origin RPS during a\n# staging purge instead of trusting a single header read.\n","bash",[27,423,424,429,444,458,463,468],{"__ignoreMap":297},[301,425,426],{"class":126,"line":303},[301,427,428],{"class":306},"# Baseline edge behaviour for a representative asset\n",[301,430,431,434,438,441],{"class":126,"line":310},[301,432,255],{"class":433},"seIZK",[301,435,437],{"class":436},"sf6mN"," -sI",[301,439,440],{"class":320}," https:\u002F\u002Fyour-domain.com\u002Fproducts\u002F42",[301,442,443],{"class":313}," \\\n",[301,445,446,449,452,455],{"class":126,"line":328},[301,447,448],{"class":313},"  |",[301,450,451],{"class":433}," grep",[301,453,454],{"class":436}," -Ei",[301,456,457],{"class":320}," \"cache-control|age|x-cache|cf-cache-status|surrogate-key\"\n",[301,459,460],{"class":126,"line":348},[301,461,462],{"class":306},"# trade-off: a one-shot curl shows steady-state freshness but NOT the\n",[301,464,465],{"class":126,"line":361},[301,466,467],{"class":306},"# origin spike a broad purge causes — for that, watch origin RPS during a\n",[301,469,470],{"class":126,"line":367},[301,471,472],{"class":306},"# staging purge instead of trusting a single header read.\n",[14,474,475,476,478,479,482,483,485],{},"Record: edge hit ratio (CDN analytics, target ",[27,477,33],{},"), ",[27,480,481],{},"Age"," header progression on repeat requests (confirms the edge is actually caching), and — critically — origin requests-per-second in the 60 seconds after a test purge. That last number is your thundering-herd indicator. If a deploy purge drives origin RPS past what it serves inside ",[27,484,45],{},", your purge is too broad or lacks a stale-serving cushion.",[177,487,489],{"id":488},"_3-isolate-the-bottleneck-choose-the-purge-primitive","3. Isolate the Bottleneck: Choose the Purge Primitive",[14,491,492],{},"Every invalidation maps to one of four primitives. Picking the wrong one is the root cause of nearly every stale-content or origin-spike incident.",[494,495,496,515],"table",{},[497,498,499],"thead",{},[500,501,502,506,509,512],"tr",{},[503,504,505],"th",{},"Primitive",[503,507,508],{},"Blast radius",[503,510,511],{},"When to use",[503,513,514],{},"Failure mode",[516,517,518,539,555,571],"tbody",{},[500,519,520,526,529,532],{},[521,522,523],"td",{},[192,524,525],{},"Purge by URL",[521,527,528],{},"One object",[521,530,531],{},"A single known page\u002Fasset changed",[521,533,534,535,538],{},"Misses variants (query strings, ",[27,536,537],{},"Vary"," permutations) → under-purge",[500,540,541,546,549,552],{},[521,542,543],{},[192,544,545],{},"Purge by tag \u002F surrogate key",[521,547,548],{},"All objects carrying the key",[521,550,551],{},"Content with shared dependencies (a product across listings, search, sitemap)",[521,553,554],{},"Over-broad tags evict more than intended → origin spike",[500,556,557,562,565,568],{},[521,558,559],{},[192,560,561],{},"Purge everything",[521,563,564],{},"Entire cache",[521,566,567],{},"Cache-key bug, security incident, last resort",[521,569,570],{},"Guaranteed thundering herd; never in a routine deploy",[500,572,573,578,581,584],{},[521,574,575],{},[192,576,577],{},"Invalidation by omission",[521,579,580],{},"Nothing purged",[521,582,583],{},"Hashed immutable assets — new hash = new URL",[521,585,586],{},"Stale HTML still referencing old hashes → broken page",[14,588,589,590,593,594,409],{},"The decision rule: ",[192,591,592],{},"hashed static assets use omission, content uses tags, single ad-hoc fixes use URL, and purge-everything is a break-glass."," Aligning hashed-asset headers so omission works correctly is detailed in ",[18,595,597],{"href":596},"\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",[177,599,601],{"id":600},"_4-apply-the-fix-soft-purge-with-stale-while-revalidate","4. Apply the Fix: Soft Purge with Stale-While-Revalidate",[14,603,604,605,608,609,612,613,616],{},"A ",[275,606,607],{},"hard"," purge deletes the object immediately — the next request is a guaranteed miss. A ",[275,610,611],{},"soft"," purge marks the object stale but keeps it on disk, letting the edge serve the stale copy once while it revalidates against origin in the background. Combined with ",[27,614,615],{},"stale-while-revalidate",", soft purge converts a synchronized miss storm into a smooth, lazy refresh.",[292,618,620],{"className":419,"code":619,"language":421,"meta":297,"style":297},"# Fastly soft purge by surrogate key — evict product-42 across all URLs\ncurl -X POST \"https:\u002F\u002Fapi.fastly.com\u002Fservice\u002F$FASTLY_SERVICE_ID\u002Fpurge\u002Fproduct-42\" \\\n  -H \"Fastly-Key: $FASTLY_PURGE_TOKEN\" \\\n  -H \"Fastly-Soft-Purge: 1\"\n# trade-off: soft purge serves ONE stale response per object during\n# revalidation. Do NOT soft-purge a security-sensitive change (leaked\n# token, wrong price) where even a single stale hit is unacceptable —\n# use a hard purge there and eat the brief origin load.\n",[27,621,622,627,648,664,671,676,681,686],{"__ignoreMap":297},[301,623,624],{"class":126,"line":303},[301,625,626],{"class":306},"# Fastly soft purge by surrogate key — evict product-42 across all URLs\n",[301,628,629,631,634,637,640,643,646],{"class":126,"line":310},[301,630,255],{"class":433},[301,632,633],{"class":436}," -X",[301,635,636],{"class":320}," POST",[301,638,639],{"class":320}," \"https:\u002F\u002Fapi.fastly.com\u002Fservice\u002F",[301,641,642],{"class":324},"$FASTLY_SERVICE_ID",[301,644,645],{"class":320},"\u002Fpurge\u002Fproduct-42\"",[301,647,443],{"class":313},[301,649,650,653,656,659,662],{"class":126,"line":328},[301,651,652],{"class":436},"  -H",[301,654,655],{"class":320}," \"Fastly-Key: ",[301,657,658],{"class":324},"$FASTLY_PURGE_TOKEN",[301,660,661],{"class":320},"\"",[301,663,443],{"class":313},[301,665,666,668],{"class":126,"line":348},[301,667,652],{"class":436},[301,669,670],{"class":320}," \"Fastly-Soft-Purge: 1\"\n",[301,672,673],{"class":126,"line":361},[301,674,675],{"class":306},"# trade-off: soft purge serves ONE stale response per object during\n",[301,677,678],{"class":126,"line":367},[301,679,680],{"class":306},"# revalidation. Do NOT soft-purge a security-sensitive change (leaked\n",[301,682,683],{"class":126,"line":373},[301,684,685],{"class":306},"# token, wrong price) where even a single stale hit is unacceptable —\n",[301,687,688],{"class":126,"line":379},[301,689,690],{"class":306},"# use a hard purge there and eat the brief origin load.\n",[14,692,693],{},"For this to be safe, the cached response must declare a stale window:",[292,695,699],{"className":696,"code":698,"language":78,"meta":297},[697],"language-text","Cache-Control: public, s-maxage=86400, stale-while-revalidate=600, stale-if-error=86400\n# trade-off: the 600s SWR window absorbs the post-purge refetch wave, but\n# means a soft-purged object can serve stale for up to 10 minutes under\n# load. Shrink the window for fast-moving content; widen it for catalogs.\n",[27,700,698],{"__ignoreMap":297},[14,702,703,704,706,707,710,711,409],{},"The ",[27,705,615],{}," mechanics — how the edge counts the window and how it interacts with ",[27,708,709],{},"s-maxage"," — are covered in ",[18,712,714],{"href":713},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002F","Stale-While-Revalidate Implementation",[177,716,718],{"id":717},"deconstructing-invalidation-latency-into-phases","Deconstructing Invalidation Latency into Phases",[14,720,721],{},"\"Did my purge work?\" decomposes into measurable phases, each with its own budget and its own failure mode. Treat them like timing phases on a Core Web Vital: find the dominant one and fix it first.",[185,723,724,730,736,742],{},[188,725,726,729],{},[192,727,728],{},"API acceptance (≤200ms):"," time for the CDN to acknowledge the purge call. A slow or rate-limited API here means your deploy job blocks or times out. Batch keys to stay under per-request limits.",[188,731,732,735],{},[192,733,734],{},"Edge propagation (\u003C2s):"," time for the eviction to reach all PoPs. Fastly propagates globally in roughly 150ms; Cloudflare and CloudFront are slower and eventually-consistent. If you read a header from a near PoP it may already be fresh while a far PoP still serves stale.",[188,737,738,741],{},[192,739,740],{},"Origin refetch (bounded by SWR window):"," the lazy refill. This is where thundering herd lives. Without soft purge + SWR, this phase collapses into a synchronized spike; with them, it spreads across the window.",[188,743,744,747,748,751],{},[192,745,746],{},"Client convergence (variable):"," browser HTTP cache and Service Worker caches do not see edge purges at all. They hold their own copies until ",[27,749,750],{},"max-age"," lapses or the SW updates its manifest.",[14,753,754],{},"The last phase is the one teams forget: an edge purge never reaches the client. Coordinating the SW tier is the subject of the advanced diagnostics below.",[177,756,758],{"id":757},"advanced-diagnostics-coordinating-origin-edge-and-service-worker-caches","Advanced Diagnostics: Coordinating Origin, Edge, and Service Worker Caches",[14,760,761,762,764,765,767],{},"A purge that fixes the edge but leaves the Service Worker serving a stale precached ",[27,763,29],{}," produces the most confusing class of bug — it reproduces only for returning users and is invisible to ",[27,766,255],{},". There are three caches in the chain, and a purge primitive only touches one of them.",[292,769,773],{"className":770,"code":771,"language":772,"meta":297,"style":297},"language-javascript shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","\u002F\u002F On SW activation, drop precaches that don't match the new build's revision\nself.addEventListener('activate', (event) => {\n  const currentCaches = [`precache-${BUILD_REVISION}`];\n  event.waitUntil(\n    caches.keys().then((keys) =>\n      Promise.all(\n        keys\n          .filter((key) => !currentCaches.includes(key))\n          .map((key) => caches.delete(key))\n      )\n    ).then(() => self.clients.claim())\n  );\n  \u002F\u002F trade-off: clients.claim() forces the new SW to control open tabs\n  \u002F\u002F immediately, but mid-session asset swaps can mix old HTML with new\n  \u002F\u002F chunks. Skip claim() if your app cannot tolerate a live version flip.\n});\n","javascript",[27,774,775,780,810,836,847,871,883,888,917,940,946,968,974,980,986,992],{"__ignoreMap":297},[301,776,777],{"class":126,"line":303},[301,778,779],{"class":306},"\u002F\u002F On SW activation, drop precaches that don't match the new build's revision\n",[301,781,782,785,789,792,795,798,801,804,807],{"class":126,"line":310},[301,783,784],{"class":324},"self.",[301,786,788],{"class":787},"ssM3C","addEventListener",[301,790,791],{"class":324},"(",[301,793,794],{"class":320},"'activate'",[301,796,797],{"class":324},", (",[301,799,800],{"class":433},"event",[301,802,803],{"class":324},") ",[301,805,806],{"class":313},"=>",[301,808,809],{"class":324}," {\n",[301,811,812,815,818,821,824,827,830,833],{"class":126,"line":328},[301,813,814],{"class":313},"  const",[301,816,817],{"class":436}," currentCaches",[301,819,820],{"class":313}," =",[301,822,823],{"class":324}," [",[301,825,826],{"class":320},"`precache-${",[301,828,829],{"class":436},"BUILD_REVISION",[301,831,832],{"class":320},"}`",[301,834,835],{"class":324},"];\n",[301,837,838,841,844],{"class":126,"line":348},[301,839,840],{"class":324},"  event.",[301,842,843],{"class":787},"waitUntil",[301,845,846],{"class":324},"(\n",[301,848,849,852,855,858,861,864,866,868],{"class":126,"line":361},[301,850,851],{"class":324},"    caches.",[301,853,854],{"class":787},"keys",[301,856,857],{"class":324},"().",[301,859,860],{"class":787},"then",[301,862,863],{"class":324},"((",[301,865,854],{"class":433},[301,867,803],{"class":324},[301,869,870],{"class":313},"=>\n",[301,872,873,876,878,881],{"class":126,"line":367},[301,874,875],{"class":436},"      Promise",[301,877,409],{"class":324},[301,879,880],{"class":787},"all",[301,882,846],{"class":324},[301,884,885],{"class":126,"line":373},[301,886,887],{"class":324},"        keys\n",[301,889,890,893,896,898,901,903,905,908,911,914],{"class":126,"line":379},[301,891,892],{"class":324},"          .",[301,894,895],{"class":787},"filter",[301,897,863],{"class":324},[301,899,900],{"class":433},"key",[301,902,803],{"class":324},[301,904,806],{"class":313},[301,906,907],{"class":313}," !",[301,909,910],{"class":324},"currentCaches.",[301,912,913],{"class":787},"includes",[301,915,916],{"class":324},"(key))\n",[301,918,919,921,924,926,928,930,932,935,938],{"class":126,"line":388},[301,920,892],{"class":324},[301,922,923],{"class":787},"map",[301,925,863],{"class":324},[301,927,900],{"class":433},[301,929,803],{"class":324},[301,931,806],{"class":313},[301,933,934],{"class":324}," caches.",[301,936,937],{"class":787},"delete",[301,939,916],{"class":324},[301,941,943],{"class":126,"line":942},10,[301,944,945],{"class":324},"      )\n",[301,947,949,952,954,957,959,962,965],{"class":126,"line":948},11,[301,950,951],{"class":324},"    ).",[301,953,860],{"class":787},[301,955,956],{"class":324},"(() ",[301,958,806],{"class":313},[301,960,961],{"class":324}," self.clients.",[301,963,964],{"class":787},"claim",[301,966,967],{"class":324},"())\n",[301,969,971],{"class":126,"line":970},12,[301,972,973],{"class":324},"  );\n",[301,975,977],{"class":126,"line":976},13,[301,978,979],{"class":306},"  \u002F\u002F trade-off: clients.claim() forces the new SW to control open tabs\n",[301,981,983],{"class":126,"line":982},14,[301,984,985],{"class":306},"  \u002F\u002F immediately, but mid-session asset swaps can mix old HTML with new\n",[301,987,989],{"class":126,"line":988},15,[301,990,991],{"class":306},"  \u002F\u002F chunks. Skip claim() if your app cannot tolerate a live version flip.\n",[301,993,995],{"class":126,"line":994},16,[301,996,997],{"class":324},"});\n",[14,999,1000],{},"The coordination rule across tiers:",[185,1002,1003,1009,1015],{},[188,1004,1005,1008],{},[192,1006,1007],{},"Origin"," is invalidated by deploying new content (and tagging it).",[188,1010,1011,1014],{},[192,1012,1013],{},"Edge"," is invalidated by tag\u002FURL\u002Fsoft purge as chosen in step 3.",[188,1016,1017,1020],{},[192,1018,1019],{},"Service Worker"," is invalidated by regenerating the precache manifest with a new revision per build, plus the cleanup above. The HTML it serves must point at the new hashes the moment the edge does.",[14,1022,1023,1024,1027,1028,1031,1032,1036,1037,409],{},"When the SW intercepts hashed assets, ensure its ",[27,1025,1026],{},"fetch"," handler does not shadow the edge's ",[27,1029,1030],{},"immutable"," responses — the bypass pattern is detailed in ",[18,1033,1035],{"href":1034},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002F","Service Worker Caching Strategies",". The most common production incident here is over- or under-purging on deploy, which has its own runbook: ",[18,1038,1040],{"href":1039},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F","purging CDN cache by tag on deploy",[177,1042,1044],{"id":1043},"validation-budgeting-assert-purge-behavior-in-ci","Validation & Budgeting: Assert Purge Behavior in CI",[14,1046,1047],{},"Invalidation correctness should fail the pipeline, not the user. Two assertions belong in every deploy: that the freshly deployed page is actually served fresh from the edge, and that the deploy did not trip a purge-everything fallback.",[292,1049,1051],{"className":419,"code":1050,"language":421,"meta":297,"style":297},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# ci\u002Fverify-purge.sh — run after the deploy's purge step\nset -euo pipefail\nURL=\"https:\u002F\u002Fyour-domain.com\u002Fproducts\u002F42\"\n\n# 1. First request after purge should MISS (proves the purge landed)\nstatus=$(curl -sI \"$URL\" | grep -i 'x-cache' | tr -d '\\r')\necho \"post-purge: $status\"\necho \"$status\" | grep -qiE 'miss|expired' || { echo \"FAIL: object still cached, purge did not land\"; exit 1; }\n\n# 2. Second request should HIT within a second (proves edge re-caches, no per-request origin load)\nsleep 1\ncurl -sI \"$URL\" | grep -i 'x-cache' | grep -qiE 'hit' || { echo \"FAIL: edge not re-caching after purge\"; exit 1; }\n# trade-off: this asserts correctness on ONE representative URL. Do NOT\n# assert it on hundreds in CI — that itself becomes a load test. Sample\n# one URL per tag class and trust tag semantics for the rest.\n",[27,1052,1053,1058,1063,1074,1085,1091,1096,1143,1157,1200,1204,1209,1217,1263,1268,1273],{"__ignoreMap":297},[301,1054,1055],{"class":126,"line":303},[301,1056,1057],{"class":306},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[301,1059,1060],{"class":126,"line":310},[301,1061,1062],{"class":306},"# ci\u002Fverify-purge.sh — run after the deploy's purge step\n",[301,1064,1065,1068,1071],{"class":126,"line":328},[301,1066,1067],{"class":436},"set",[301,1069,1070],{"class":436}," -euo",[301,1072,1073],{"class":320}," pipefail\n",[301,1075,1076,1079,1082],{"class":126,"line":348},[301,1077,1078],{"class":324},"URL",[301,1080,1081],{"class":313},"=",[301,1083,1084],{"class":320},"\"https:\u002F\u002Fyour-domain.com\u002Fproducts\u002F42\"\n",[301,1086,1087],{"class":126,"line":361},[301,1088,1090],{"emptyLinePlaceholder":1089},true,"\n",[301,1092,1093],{"class":126,"line":367},[301,1094,1095],{"class":306},"# 1. First request after purge should MISS (proves the purge landed)\n",[301,1097,1098,1101,1103,1106,1108,1110,1113,1116,1118,1121,1123,1126,1129,1131,1134,1137,1140],{"class":126,"line":373},[301,1099,1100],{"class":324},"status",[301,1102,1081],{"class":313},[301,1104,1105],{"class":324},"$(",[301,1107,255],{"class":433},[301,1109,437],{"class":436},[301,1111,1112],{"class":320}," \"",[301,1114,1115],{"class":324},"$URL",[301,1117,661],{"class":320},[301,1119,1120],{"class":313}," |",[301,1122,451],{"class":433},[301,1124,1125],{"class":436}," -i",[301,1127,1128],{"class":320}," 'x-cache'",[301,1130,1120],{"class":313},[301,1132,1133],{"class":433}," tr",[301,1135,1136],{"class":436}," -d",[301,1138,1139],{"class":320}," '\\r'",[301,1141,1142],{"class":324},")\n",[301,1144,1145,1148,1151,1154],{"class":126,"line":379},[301,1146,1147],{"class":436},"echo",[301,1149,1150],{"class":320}," \"post-purge: ",[301,1152,1153],{"class":324},"$status",[301,1155,1156],{"class":320},"\"\n",[301,1158,1159,1161,1163,1165,1167,1169,1171,1174,1177,1180,1183,1185,1188,1191,1194,1197],{"class":126,"line":388},[301,1160,1147],{"class":436},[301,1162,1112],{"class":320},[301,1164,1153],{"class":324},[301,1166,661],{"class":320},[301,1168,1120],{"class":313},[301,1170,451],{"class":433},[301,1172,1173],{"class":436}," -qiE",[301,1175,1176],{"class":320}," 'miss|expired'",[301,1178,1179],{"class":313}," ||",[301,1181,1182],{"class":324}," { ",[301,1184,1147],{"class":436},[301,1186,1187],{"class":320}," \"FAIL: object still cached, purge did not land\"",[301,1189,1190],{"class":324},"; ",[301,1192,1193],{"class":436},"exit",[301,1195,1196],{"class":436}," 1",[301,1198,1199],{"class":324},"; }\n",[301,1201,1202],{"class":126,"line":942},[301,1203,1090],{"emptyLinePlaceholder":1089},[301,1205,1206],{"class":126,"line":948},[301,1207,1208],{"class":306},"# 2. Second request should HIT within a second (proves edge re-caches, no per-request origin load)\n",[301,1210,1211,1214],{"class":126,"line":970},[301,1212,1213],{"class":433},"sleep",[301,1215,1216],{"class":436}," 1\n",[301,1218,1219,1221,1223,1225,1227,1229,1231,1233,1235,1237,1239,1241,1243,1246,1248,1250,1252,1255,1257,1259,1261],{"class":126,"line":976},[301,1220,255],{"class":433},[301,1222,437],{"class":436},[301,1224,1112],{"class":320},[301,1226,1115],{"class":324},[301,1228,661],{"class":320},[301,1230,1120],{"class":313},[301,1232,451],{"class":433},[301,1234,1125],{"class":436},[301,1236,1128],{"class":320},[301,1238,1120],{"class":313},[301,1240,451],{"class":433},[301,1242,1173],{"class":436},[301,1244,1245],{"class":320}," 'hit'",[301,1247,1179],{"class":313},[301,1249,1182],{"class":324},[301,1251,1147],{"class":436},[301,1253,1254],{"class":320}," \"FAIL: edge not re-caching after purge\"",[301,1256,1190],{"class":324},[301,1258,1193],{"class":436},[301,1260,1196],{"class":436},[301,1262,1199],{"class":324},[301,1264,1265],{"class":126,"line":982},[301,1266,1267],{"class":306},"# trade-off: this asserts correctness on ONE representative URL. Do NOT\n",[301,1269,1270],{"class":126,"line":988},[301,1271,1272],{"class":306},"# assert it on hundreds in CI — that itself becomes a load test. Sample\n",[301,1274,1275],{"class":126,"line":994},[301,1276,1277],{"class":306},"# one URL per tag class and trust tag semantics for the rest.\n",[14,1279,1280,1281,1283,1284,1286,1287,1289],{},"Set explicit budgets and alert on regressions: edge hit ratio ",[27,1282,33],{}," sustained, cached TTFB ",[27,1285,37],{},", post-deploy origin RPS within origin headroom, and purge-to-fresh propagation ",[27,1288,41],{},". Wire the script above into the same CI stage that runs your header audits so a purge that silently fails to land blocks the release rather than reaching production.",[177,1291,1293],{"id":1292},"related","Related",[185,1295,1296,1301,1315,1320,1325],{},[188,1297,1298,1300],{},[18,1299,408],{"href":407}," — set cache keys and TTLs so tags and surrogate keys resolve to the objects you expect.",[188,1302,1303,1307,1308,282,1310,282,1312,1314],{},[18,1304,1306],{"href":1305},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fhttp-cache-control-headers-explained\u002F","HTTP Cache-Control Headers Explained"," — the directive precedence (",[27,1309,709],{},[27,1311,1030],{},[27,1313,615],{},") that purge strategies build on.",[188,1316,1317,1319],{},[18,1318,714],{"href":713}," — the stale-serving window that turns a soft purge into a smooth refill.",[188,1321,1322,1324],{},[18,1323,1035],{"href":1034}," — invalidating the client tier that edge purges can never reach.",[188,1326,1327,1330],{},[18,1328,1329],{"href":1039},"Purging CDN cache by tag on deploy"," — the deploy-time runbook for over- and under-purging.",[1332,1333,1335],"script",{"type":1334},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@graph\": [\n    {\n      \"@type\": \"TechArticle\",\n      \"headline\": \"Cache Invalidation Patterns\",\n      \"description\": \"A diagnostic workflow for invalidating CDN and edge caches with cache tags, surrogate keys, soft purge, and content-hash omission without stale content or thundering-herd revalidation.\",\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\u002F\"\n    },\n    {\n      \"@type\": \"HowTo\",\n      \"name\": \"Invalidate CDN edge caches without stale content or origin stampede\",\n      \"step\": [\n        { \"@type\": \"HowToStep\", \"name\": \"Tag responses with surrogate keys\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F#1-environment-setup-tag-responses-with-surrogate-keys\" },\n        { \"@type\": \"HowToStep\", \"name\": \"Capture baseline hit ratio and post-purge origin load\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F#2-capture-baseline-measure-hit-ratio-and-post-purge-origin-load\" },\n        { \"@type\": \"HowToStep\", \"name\": \"Choose the purge primitive\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F#3-isolate-the-bottleneck-choose-the-purge-primitive\" },\n        { \"@type\": \"HowToStep\", \"name\": \"Apply soft purge with stale-while-revalidate\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F#4-apply-the-fix-soft-purge-with-stale-while-revalidate\" }\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      ]\n    }\n  ]\n}\n",[1337,1338,1339],"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":297,"searchDepth":310,"depth":310,"links":1341},[1342,1343,1344,1345,1346,1347,1348,1349,1350],{"id":179,"depth":310,"text":180},{"id":269,"depth":310,"text":270},{"id":412,"depth":310,"text":413},{"id":488,"depth":310,"text":489},{"id":600,"depth":310,"text":601},{"id":717,"depth":310,"text":718},{"id":757,"depth":310,"text":758},{"id":1043,"depth":310,"text":1044},{"id":1292,"depth":310,"text":1293},"A diagnostic workflow for invalidating CDN and edge caches without stale content or thundering-herd revalidation.","md",{"slug":12,"type":1354,"breadcrumb":1355,"datePublished":1361,"dateModified":1361},"cluster",[1356,1358,1359],{"name":1357,"url":239},"Home",{"name":21,"url":20},{"name":5,"url":1360},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F","2026-06-18","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns",{"title":1364,"description":1365},"Cache Invalidation Patterns at the CDN Edge","Master CDN cache invalidation: cache tags, surrogate keys, purge-by-URL vs purge-by-tag, soft purge, and coordinating origin, edge, and Service Worker caches.","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Findex","g2ASP6i978rDc6H7gLjyCLzjRnPW_M3siWZX5xtoVkU",[1369,1372],{"title":21,"path":1370,"stem":1371,"children":-1},"\u002Fadvanced-caching-strategies-cdn-architecture","advanced-caching-strategies-cdn-architecture\u002Findex",{"title":1373,"path":1374,"stem":1375,"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",1782237170939]