[{"data":1,"prerenderedAt":1763},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns":1756},{"id":4,"title":5,"body":6,"description":1738,"extension":1739,"meta":1740,"navigation":1478,"path":1750,"seo":1751,"stem":1754,"__hash__":1755},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Findex.md","Cache Invalidation Patterns",{"type":7,"value":8,"toc":1724},"minimark",[9,13,23,47,176,181,184,264,267,271,291,392,401,494,500,534,538,541,597,610,614,617,711,731,742,821,825,840,910,913,920,934,938,941,971,974,978,987,1113,1118,1152,1156,1159,1388,1391,1411,1430,1434,1437,1667,1679,1683,1715,1720],[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],{},"The header name is CDN-specific. Fastly reads ",[27,396,198],{}," (space-delimited); Cloudflare Enterprise reads ",[27,399,209],{}," (comma-delimited). The same origin route emits both so the config is portable across edges:",[292,402,404],{"className":294,"code":403,"language":296,"meta":297,"style":297},"# Portable tagging — emit both header dialects; each edge reads only its own\nlocation ~ ^\u002Fproducts\u002F(\\d+)$ {\n  add_header Surrogate-Key \"product-$1 category-shoes layout-v3\";  # Fastly\n  add_header Cache-Tag     \"product-$1,category-shoes,layout-v3\";  # Cloudflare\n  add_header Cache-Control \"public, max-age=0, s-maxage=86400\";\n  # trade-off: emitting both is free, but the tag VOCABULARY must stay\n  # identical across services — a purge script that targets `product-42`\n  # on Fastly and `product_42` on Cloudflare silently under-purges one edge.\n  proxy_pass http:\u002F\u002Fbackend;\n}\n",[27,405,406,411,421,439,458,468,473,478,483,489],{"__ignoreMap":297},[301,407,408],{"class":126,"line":303},[301,409,410],{"class":306},"# Portable tagging — emit both header dialects; each edge reads only its own\n",[301,412,413,415,417,419],{"class":126,"line":310},[301,414,314],{"class":313},[301,416,317],{"class":313},[301,418,321],{"class":320},[301,420,325],{"class":324},[301,422,423,425,427,429,431,433,436],{"class":126,"line":328},[301,424,331],{"class":313},[301,426,334],{"class":324},[301,428,337],{"class":320},[301,430,70],{"class":324},[301,432,342],{"class":320},[301,434,435],{"class":324},";  ",[301,437,438],{"class":306},"# Fastly\n",[301,440,441,443,446,448,450,453,455],{"class":126,"line":348},[301,442,331],{"class":313},[301,444,445],{"class":324},"Cache-Tag     ",[301,447,337],{"class":320},[301,449,70],{"class":324},[301,451,452],{"class":320},",category-shoes,layout-v3\"",[301,454,435],{"class":324},[301,456,457],{"class":306},"# Cloudflare\n",[301,459,460,462,464,466],{"class":126,"line":361},[301,461,331],{"class":313},[301,463,353],{"class":324},[301,465,356],{"class":320},[301,467,345],{"class":324},[301,469,470],{"class":126,"line":367},[301,471,472],{"class":306},"  # trade-off: emitting both is free, but the tag VOCABULARY must stay\n",[301,474,475],{"class":126,"line":373},[301,476,477],{"class":306},"  # identical across services — a purge script that targets `product-42`\n",[301,479,480],{"class":126,"line":379},[301,481,482],{"class":306},"  # on Fastly and `product_42` on Cloudflare silently under-purges one edge.\n",[301,484,485,487],{"class":126,"line":388},[301,486,382],{"class":313},[301,488,385],{"class":324},[301,490,492],{"class":126,"line":491},10,[301,493,391],{"class":324},[14,495,496,497,499],{},"Most edges strip the header before it reaches the browser, so it costs nothing on the wire. Two hard limits shape how many keys you can attach: Cloudflare caps ",[27,498,209],{}," at 16 KB of header value on the origin response (roughly a thousand short tags) and enforces its own purge-API rate limits, while Fastly counts surrogate keys against the 8 KB header budget. Stay well under these — a response carrying hundreds of tags is a signal your tags are too fine-grained, not that you need a bigger header.",[14,501,502,503,506,507,509,510,512,513,515,516,520,521,524,525,528,529,533],{},"The discipline that matters: tag by ",[275,504,505],{},"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. Pick a small, stable vocabulary — an entity key (",[27,508,281],{},"), a collection key (",[27,511,285],{},"), and a template key (",[27,514,289],{},") — so any content change maps to an obvious purge target. Header placement and edge cache-key alignment are covered in depth in ",[18,517,519],{"href":518},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002F","CDN Edge Caching Configuration",", and the directive precedence that makes ",[27,522,523],{},"s-maxage=86400"," outrank the browser's ",[27,526,527],{},"max-age=0"," is unpacked in ",[18,530,532],{"href":531},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fhttp-cache-control-headers-explained\u002F","HTTP Cache-Control Headers Explained",".",[177,535,537],{"id":536},"_2-capture-baseline-measure-hit-ratio-and-post-purge-origin-load","2. Capture Baseline: Measure Hit Ratio and Post-Purge Origin Load",[14,539,540],{},"You cannot tell whether a purge strategy is over- or under-firing without numbers. Capture three baselines before changing anything.",[292,542,546],{"className":543,"code":544,"language":545,"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,547,548,553,568,582,587,592],{"__ignoreMap":297},[301,549,550],{"class":126,"line":303},[301,551,552],{"class":306},"# Baseline edge behaviour for a representative asset\n",[301,554,555,558,562,565],{"class":126,"line":310},[301,556,255],{"class":557},"seIZK",[301,559,561],{"class":560},"sf6mN"," -sI",[301,563,564],{"class":320}," https:\u002F\u002Fyour-domain.com\u002Fproducts\u002F42",[301,566,567],{"class":313}," \\\n",[301,569,570,573,576,579],{"class":126,"line":328},[301,571,572],{"class":313},"  |",[301,574,575],{"class":557}," grep",[301,577,578],{"class":560}," -Ei",[301,580,581],{"class":320}," \"cache-control|age|x-cache|cf-cache-status|surrogate-key\"\n",[301,583,584],{"class":126,"line":348},[301,585,586],{"class":306},"# trade-off: a one-shot curl shows steady-state freshness but NOT the\n",[301,588,589],{"class":126,"line":361},[301,590,591],{"class":306},"# origin spike a broad purge causes — for that, watch origin RPS during a\n",[301,593,594],{"class":126,"line":367},[301,595,596],{"class":306},"# staging purge instead of trusting a single header read.\n",[14,598,599,600,602,603,606,607,609],{},"Record: edge hit ratio (CDN analytics, target ",[27,601,33],{},"), ",[27,604,605],{},"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,608,45],{},", your purge is too broad or lacks a stale-serving cushion.",[177,611,613],{"id":612},"_3-isolate-the-bottleneck-choose-the-purge-primitive","3. Isolate the Bottleneck: Choose the Purge Primitive",[14,615,616],{},"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.",[618,619,620,639],"table",{},[621,622,623],"thead",{},[624,625,626,630,633,636],"tr",{},[627,628,629],"th",{},"Primitive",[627,631,632],{},"Blast radius",[627,634,635],{},"When to use",[627,637,638],{},"Failure mode",[640,641,642,663,679,695],"tbody",{},[624,643,644,650,653,656],{},[645,646,647],"td",{},[192,648,649],{},"Purge by URL",[645,651,652],{},"One object",[645,654,655],{},"A single known page\u002Fasset changed",[645,657,658,659,662],{},"Misses variants (query strings, ",[27,660,661],{},"Vary"," permutations) → under-purge",[624,664,665,670,673,676],{},[645,666,667],{},[192,668,669],{},"Purge by tag \u002F surrogate key",[645,671,672],{},"All objects carrying the key",[645,674,675],{},"Content with shared dependencies (a product across listings, search, sitemap)",[645,677,678],{},"Over-broad tags evict more than intended → origin spike",[624,680,681,686,689,692],{},[645,682,683],{},[192,684,685],{},"Purge everything",[645,687,688],{},"Entire cache",[645,690,691],{},"Cache-key bug, security incident, last resort",[645,693,694],{},"Guaranteed thundering herd; never in a routine deploy",[624,696,697,702,705,708],{},[645,698,699],{},[192,700,701],{},"Invalidation by omission",[645,703,704],{},"Nothing purged",[645,706,707],{},"Hashed immutable assets — new hash = new URL",[645,709,710],{},"Stale HTML still referencing old hashes → broken page",[14,712,713,714,717,718,722,723,726,727,533],{},"The decision rule: ",[192,715,716],{},"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,719,721],{"href":720},"\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 sharp edge — how a build that reuses a filename resurrects a stale byte range under ",[27,724,725],{},"immutable"," — has its own runbook in ",[18,728,730],{"href":729},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F","invalidating immutable hashed assets safely",[14,732,733,734,737,738,741],{},"Primitive choice also depends on how your CDN propagates the purge. Fastly's tag purge is strongly consistent and global in roughly 150ms, so a tag purge is cheap enough to fire on every content edit. Cloudflare and CloudFront are eventually consistent and coarser: CloudFront has no native tagging at all — you invalidate by path pattern (",[27,735,736],{},"\u002Fproducts\u002F*","), the first 1,000 invalidation paths per month are free and further ones are billed, and each invalidation takes tens of seconds to complete. On those edges, batch related changes into one purge call rather than firing one per entity, and never put a ",[27,739,740],{},"\u002F*"," invalidation in a hot deploy path — it is a purge-everything in disguise and will stampede your origin exactly like the last table row warns.",[292,743,745],{"className":543,"code":744,"language":545,"meta":297,"style":297},"# Cloudflare tag purge — evict every object carrying Cache-Tag: product-42\ncurl -X POST \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F$CF_ZONE_ID\u002Fpurge_cache\" \\\n  -H \"Authorization: Bearer $CF_PURGE_TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  --data '{\"tags\":[\"product-42\"]}'\n# trade-off: `tags` is Enterprise-only. On lower Cloudflare plans you are\n# limited to purge-by-URL and purge-everything — design invalidation around\n# hashed-asset omission there, because tag granularity simply is not available.\n",[27,746,747,752,773,789,798,806,811,816],{"__ignoreMap":297},[301,748,749],{"class":126,"line":303},[301,750,751],{"class":306},"# Cloudflare tag purge — evict every object carrying Cache-Tag: product-42\n",[301,753,754,756,759,762,765,768,771],{"class":126,"line":310},[301,755,255],{"class":557},[301,757,758],{"class":560}," -X",[301,760,761],{"class":320}," POST",[301,763,764],{"class":320}," \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F",[301,766,767],{"class":324},"$CF_ZONE_ID",[301,769,770],{"class":320},"\u002Fpurge_cache\"",[301,772,567],{"class":313},[301,774,775,778,781,784,787],{"class":126,"line":328},[301,776,777],{"class":560},"  -H",[301,779,780],{"class":320}," \"Authorization: Bearer ",[301,782,783],{"class":324},"$CF_PURGE_TOKEN",[301,785,786],{"class":320},"\"",[301,788,567],{"class":313},[301,790,791,793,796],{"class":126,"line":348},[301,792,777],{"class":560},[301,794,795],{"class":320}," \"Content-Type: application\u002Fjson\"",[301,797,567],{"class":313},[301,799,800,803],{"class":126,"line":361},[301,801,802],{"class":560},"  --data",[301,804,805],{"class":320}," '{\"tags\":[\"product-42\"]}'\n",[301,807,808],{"class":126,"line":367},[301,809,810],{"class":306},"# trade-off: `tags` is Enterprise-only. On lower Cloudflare plans you are\n",[301,812,813],{"class":126,"line":373},[301,814,815],{"class":306},"# limited to purge-by-URL and purge-everything — design invalidation around\n",[301,817,818],{"class":126,"line":379},[301,819,820],{"class":306},"# hashed-asset omission there, because tag granularity simply is not available.\n",[177,822,824],{"id":823},"_4-apply-the-fix-soft-purge-with-stale-while-revalidate","4. Apply the Fix: Soft Purge with Stale-While-Revalidate",[14,826,827,828,831,832,835,836,839],{},"A ",[275,829,830],{},"hard"," purge deletes the object immediately — the next request is a guaranteed miss. A ",[275,833,834],{},"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,837,838],{},"stale-while-revalidate",", soft purge converts a synchronized miss storm into a smooth, lazy refresh.",[292,841,843],{"className":543,"code":842,"language":545,"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,844,845,850,869,883,890,895,900,905],{"__ignoreMap":297},[301,846,847],{"class":126,"line":303},[301,848,849],{"class":306},"# Fastly soft purge by surrogate key — evict product-42 across all URLs\n",[301,851,852,854,856,858,861,864,867],{"class":126,"line":310},[301,853,255],{"class":557},[301,855,758],{"class":560},[301,857,761],{"class":320},[301,859,860],{"class":320}," \"https:\u002F\u002Fapi.fastly.com\u002Fservice\u002F",[301,862,863],{"class":324},"$FASTLY_SERVICE_ID",[301,865,866],{"class":320},"\u002Fpurge\u002Fproduct-42\"",[301,868,567],{"class":313},[301,870,871,873,876,879,881],{"class":126,"line":328},[301,872,777],{"class":560},[301,874,875],{"class":320}," \"Fastly-Key: ",[301,877,878],{"class":324},"$FASTLY_PURGE_TOKEN",[301,880,786],{"class":320},[301,882,567],{"class":313},[301,884,885,887],{"class":126,"line":348},[301,886,777],{"class":560},[301,888,889],{"class":320}," \"Fastly-Soft-Purge: 1\"\n",[301,891,892],{"class":126,"line":361},[301,893,894],{"class":306},"# trade-off: soft purge serves ONE stale response per object during\n",[301,896,897],{"class":126,"line":367},[301,898,899],{"class":306},"# revalidation. Do NOT soft-purge a security-sensitive change (leaked\n",[301,901,902],{"class":126,"line":373},[301,903,904],{"class":306},"# token, wrong price) where even a single stale hit is unacceptable —\n",[301,906,907],{"class":126,"line":379},[301,908,909],{"class":306},"# use a hard purge there and eat the brief origin load.\n",[14,911,912],{},"For this to be safe, the cached response must declare a stale window:",[292,914,918],{"className":915,"code":917,"language":78,"meta":297},[916],"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,919,917],{"__ignoreMap":297},[14,921,922,923,925,926,929,930,533],{},"The ",[27,924,838],{}," mechanics — how the edge counts the window and how it interacts with ",[27,927,928],{},"s-maxage"," — are covered in ",[18,931,933],{"href":932},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002F","Stale-While-Revalidate Implementation",[177,935,937],{"id":936},"deconstructing-invalidation-latency-into-phases","Deconstructing Invalidation Latency into Phases",[14,939,940],{},"\"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,942,943,949,955,961],{},[188,944,945,948],{},[192,946,947],{},"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,950,951,954],{},[192,952,953],{},"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,956,957,960],{},[192,958,959],{},"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,962,963,966,967,970],{},[192,964,965],{},"Client convergence (variable):"," browser HTTP cache and Service Worker caches do not see edge purges at all. They hold their own copies until ",[27,968,969],{},"max-age"," lapses or the SW updates its manifest.",[14,972,973],{},"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,975,977],{"id":976},"advanced-diagnostics-coordinating-origin-edge-and-service-worker-caches","Advanced Diagnostics: Coordinating Origin, Edge, and Service Worker Caches",[14,979,980,981,983,984,986],{},"A purge that fixes the edge but leaves the Service Worker serving a stale precached ",[27,982,29],{}," produces the most confusing class of bug — it reproduces only for returning users and is invisible to ",[27,985,255],{},". There are three caches in the chain, and a purge primitive only touches one of them.",[14,988,989],{},[50,990,58,993,58,996,58,999,58,1016,58,1019,58,1022,58,1027,58,1031,58,1034,58,1038,58,1042,58,1046,58,1049,58,1052,58,1059,58,1062,58,1066,58,1070,58,1072,58,1075,58,1078,58,1080,58,1084,58,1089,58,1091,58,1095,58,1099,58,1104,58,1107,58,1110,58],{"xmlns":52,"viewBox":991,"width":54,"role":55,"ariaLabel":992,"style":57},"0 0 760 372","Three-tier cache map: origin, edge PoP, and browser plus Service Worker caches each need a different invalidation trigger, and an edge purge never reaches the client tier",[60,994,995],{},"Three-tier cache invalidation map",[64,997,998],{},"Origin, edge PoP, and browser or Service Worker caches stacked as layers. Each tier is invalidated by a different trigger: a deploy plus tag at the origin, a tag or soft purge at the edge, and a new precache-manifest revision at the Service Worker. A dashed barrier shows that an edge purge never crosses into the client tier.",[1000,1001,58,1002,58],"defs",{},[1003,1004,58,1011,58],"marker",{"id":1005,"viewBox":1006,"refX":1007,"refY":1008,"markerWidth":1009,"markerHeight":1009,"orient":1010},"civ-arrow","0 0 10 10","9","5","7","auto-start-reverse",[1012,1013],"path",{"d":1014,"fill":75,"style":1015},"M0 0 L10 5 L0 10 z","fill-opacity:0.55",[68,1017],{"x":70,"y":70,"width":71,"height":1018,"rx":73,"fill":74,"stroke":75,"style":76},"370",[78,1020,1021],{"x":80,"y":81,"fill":75,"style":82},"Three caches, three invalidators",[78,1023,1026],{"x":93,"y":1024,"fill":75,"style":1025},"63","font-size:12px;font-weight:600;text-anchor:middle;fill-opacity:0.65","What invalidates it",[78,1028,1030],{"x":1029,"y":1024,"fill":75,"style":1025},"548","Cache tier",[68,1032],{"x":80,"y":1033,"width":87,"height":141,"rx":88,"fill":115,"stroke":116,"style":117},"76",[78,1035,1037],{"x":93,"y":1036,"fill":75,"style":95},"101","Deploy new content",[78,1039,1041],{"x":93,"y":1040,"fill":75,"style":100},"120","+ stamp surrogate keys",[68,1043],{"x":1044,"y":1033,"width":1045,"height":141,"rx":88,"fill":89,"stroke":89,"style":90},"360","376",[78,1047,1048],{"x":1029,"y":1036,"fill":75,"style":95},"Origin — source of truth",[78,1050,1051],{"x":1029,"y":1040,"fill":75,"style":100},"writes the authoritative bytes",[126,1053],{"x1":1054,"y1":1055,"x2":1056,"y2":1055,"stroke":75,"style":1057,"markerEnd":1058},"228","106","356","stroke-opacity:0.5","url(#civ-arrow)",[68,1060],{"x":80,"y":1061,"width":87,"height":141,"rx":88,"fill":115,"stroke":116,"style":117},"168",[78,1063,1065],{"x":93,"y":1064,"fill":75,"style":95},"193","Tag \u002F soft purge",[78,1067,1069],{"x":93,"y":1068,"fill":75,"style":100},"212","evict objects by key",[68,1071],{"x":1044,"y":1061,"width":1045,"height":141,"rx":88,"fill":89,"stroke":89,"style":90},[78,1073,1074],{"x":1029,"y":1064,"fill":75,"style":95},"Edge PoP cache",[78,1076,1077],{"x":1029,"y":1068,"fill":75,"style":100},"objects indexed by surrogate key",[126,1079],{"x1":1054,"y1":146,"x2":1056,"y2":146,"stroke":75,"style":1057,"markerEnd":1058},[126,1081],{"x1":80,"y1":164,"x2":165,"y2":164,"stroke":116,"strokeDashArray":1082,"style":1083},[88,1008],"stroke-opacity:0.7",[78,1085,1088],{"x":107,"y":1086,"fill":116,"style":1087},"252","font-size:12px;font-weight:600;text-anchor:middle","An edge purge stops here — it never crosses into the client tier",[68,1090],{"x":80,"y":130,"width":87,"height":141,"rx":88,"fill":115,"stroke":116,"style":117},[78,1092,1094],{"x":93,"y":1093,"fill":75,"style":95},"303","New precache revision",[78,1096,1098],{"x":93,"y":1097,"fill":75,"style":100},"322","+ delete old caches",[68,1100],{"x":1044,"y":130,"width":1045,"height":141,"rx":88,"fill":89,"stroke":89,"strokeDashArray":1101,"style":1103},[1008,1102],"4","fill-opacity:0.07;stroke-opacity:0.6",[78,1105,1106],{"x":1029,"y":1093,"fill":75,"style":95},"Browser HTTP + Service Worker cache",[78,1108,1109],{"x":1029,"y":1097,"fill":75,"style":100},"holds its own copy, unaware of the edge",[126,1111],{"x1":1054,"y1":1112,"x2":1056,"y2":1112,"stroke":75,"style":1057,"markerEnd":1058},"308",[1114,1115,1117],"h3",{"id":1116},"the-vary-trap-purging-one-variant-leaves-the-others-stale","The Vary trap: purging one variant leaves the others stale",[14,1119,1120,1121,1124,1125,1128,1129,239,1132,1135,1136,1139,1140,1146,1147,1149,1150,533],{},"Tag purges evict by content identity, but the edge stores a ",[275,1122,1123],{},"separate object per cache-key variant",". If a response carries ",[27,1126,1127],{},"Vary: Accept-Encoding, Accept-Language",", one logical page is several physical edge objects — ",[27,1130,1131],{},"br",[27,1133,1134],{},"gzip"," × each language. A purge by surrogate key evicts every variant that carries the key, which is correct; a purge ",[275,1137,1138],{},"by URL"," evicts only the exact variant the purge request negotiated and silently leaves the others stale. The rule that falls out: ",[192,1141,1142,1143,1145],{},"on any route with a ",[27,1144,661],{}," header, never invalidate by URL — always tag it and purge by tag",", so the eviction spans the whole variant fan-out. This is the same cache-key surface you tune when setting TTLs, which is why keeping the ",[27,1148,661],{}," axis minimal is treated as a first-class concern in ",[18,1151,519],{"href":518},[1114,1153,1155],{"id":1154},"invalidating-the-service-worker-tier","Invalidating the Service Worker tier",[14,1157,1158],{},"The Service Worker holds its own precache, keyed by build revision. It never observes an edge purge, so the only way to invalidate it is to ship a new revision and delete the old caches on activation:",[292,1160,1164],{"className":1161,"code":1162,"language":1163,"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,1165,1166,1171,1201,1227,1238,1262,1274,1279,1308,1331,1336,1358,1364,1370,1376,1382],{"__ignoreMap":297},[301,1167,1168],{"class":126,"line":303},[301,1169,1170],{"class":306},"\u002F\u002F On SW activation, drop precaches that don't match the new build's revision\n",[301,1172,1173,1176,1180,1183,1186,1189,1192,1195,1198],{"class":126,"line":310},[301,1174,1175],{"class":324},"self.",[301,1177,1179],{"class":1178},"ssM3C","addEventListener",[301,1181,1182],{"class":324},"(",[301,1184,1185],{"class":320},"'activate'",[301,1187,1188],{"class":324},", (",[301,1190,1191],{"class":557},"event",[301,1193,1194],{"class":324},") ",[301,1196,1197],{"class":313},"=>",[301,1199,1200],{"class":324}," {\n",[301,1202,1203,1206,1209,1212,1215,1218,1221,1224],{"class":126,"line":328},[301,1204,1205],{"class":313},"  const",[301,1207,1208],{"class":560}," currentCaches",[301,1210,1211],{"class":313}," =",[301,1213,1214],{"class":324}," [",[301,1216,1217],{"class":320},"`precache-${",[301,1219,1220],{"class":560},"BUILD_REVISION",[301,1222,1223],{"class":320},"}`",[301,1225,1226],{"class":324},"];\n",[301,1228,1229,1232,1235],{"class":126,"line":348},[301,1230,1231],{"class":324},"  event.",[301,1233,1234],{"class":1178},"waitUntil",[301,1236,1237],{"class":324},"(\n",[301,1239,1240,1243,1246,1249,1252,1255,1257,1259],{"class":126,"line":361},[301,1241,1242],{"class":324},"    caches.",[301,1244,1245],{"class":1178},"keys",[301,1247,1248],{"class":324},"().",[301,1250,1251],{"class":1178},"then",[301,1253,1254],{"class":324},"((",[301,1256,1245],{"class":557},[301,1258,1194],{"class":324},[301,1260,1261],{"class":313},"=>\n",[301,1263,1264,1267,1269,1272],{"class":126,"line":367},[301,1265,1266],{"class":560},"      Promise",[301,1268,533],{"class":324},[301,1270,1271],{"class":1178},"all",[301,1273,1237],{"class":324},[301,1275,1276],{"class":126,"line":373},[301,1277,1278],{"class":324},"        keys\n",[301,1280,1281,1284,1287,1289,1292,1294,1296,1299,1302,1305],{"class":126,"line":379},[301,1282,1283],{"class":324},"          .",[301,1285,1286],{"class":1178},"filter",[301,1288,1254],{"class":324},[301,1290,1291],{"class":557},"key",[301,1293,1194],{"class":324},[301,1295,1197],{"class":313},[301,1297,1298],{"class":313}," !",[301,1300,1301],{"class":324},"currentCaches.",[301,1303,1304],{"class":1178},"includes",[301,1306,1307],{"class":324},"(key))\n",[301,1309,1310,1312,1315,1317,1319,1321,1323,1326,1329],{"class":126,"line":388},[301,1311,1283],{"class":324},[301,1313,1314],{"class":1178},"map",[301,1316,1254],{"class":324},[301,1318,1291],{"class":557},[301,1320,1194],{"class":324},[301,1322,1197],{"class":313},[301,1324,1325],{"class":324}," caches.",[301,1327,1328],{"class":1178},"delete",[301,1330,1307],{"class":324},[301,1332,1333],{"class":126,"line":491},[301,1334,1335],{"class":324},"      )\n",[301,1337,1339,1342,1344,1347,1349,1352,1355],{"class":126,"line":1338},11,[301,1340,1341],{"class":324},"    ).",[301,1343,1251],{"class":1178},[301,1345,1346],{"class":324},"(() ",[301,1348,1197],{"class":313},[301,1350,1351],{"class":324}," self.clients.",[301,1353,1354],{"class":1178},"claim",[301,1356,1357],{"class":324},"())\n",[301,1359,1361],{"class":126,"line":1360},12,[301,1362,1363],{"class":324},"  );\n",[301,1365,1367],{"class":126,"line":1366},13,[301,1368,1369],{"class":306},"  \u002F\u002F trade-off: clients.claim() forces the new SW to control open tabs\n",[301,1371,1373],{"class":126,"line":1372},14,[301,1374,1375],{"class":306},"  \u002F\u002F immediately, but mid-session asset swaps can mix old HTML with new\n",[301,1377,1379],{"class":126,"line":1378},15,[301,1380,1381],{"class":306},"  \u002F\u002F chunks. Skip claim() if your app cannot tolerate a live version flip.\n",[301,1383,1385],{"class":126,"line":1384},16,[301,1386,1387],{"class":324},"});\n",[14,1389,1390],{},"The coordination rule across tiers:",[185,1392,1393,1399,1405],{},[188,1394,1395,1398],{},[192,1396,1397],{},"Origin"," is invalidated by deploying new content (and tagging it).",[188,1400,1401,1404],{},[192,1402,1403],{},"Edge"," is invalidated by tag\u002FURL\u002Fsoft purge as chosen in step 3.",[188,1406,1407,1410],{},[192,1408,1409],{},"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,1412,1413,1414,1417,1418,1420,1421,1425,1426,533],{},"When the SW intercepts hashed assets, ensure its ",[27,1415,1416],{},"fetch"," handler does not shadow the edge's ",[27,1419,725],{}," responses — the bypass pattern is detailed in ",[18,1422,1424],{"href":1423},"\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,1427,1429],{"href":1428},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F","purging CDN cache by tag on deploy",[177,1431,1433],{"id":1432},"validation-budgeting-assert-purge-behavior-in-ci","Validation & Budgeting: Assert Purge Behavior in CI",[14,1435,1436],{},"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,1438,1440],{"className":543,"code":1439,"language":545,"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,1441,1442,1447,1452,1463,1474,1480,1485,1532,1546,1589,1593,1598,1606,1652,1657,1662],{"__ignoreMap":297},[301,1443,1444],{"class":126,"line":303},[301,1445,1446],{"class":306},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[301,1448,1449],{"class":126,"line":310},[301,1450,1451],{"class":306},"# ci\u002Fverify-purge.sh — run after the deploy's purge step\n",[301,1453,1454,1457,1460],{"class":126,"line":328},[301,1455,1456],{"class":560},"set",[301,1458,1459],{"class":560}," -euo",[301,1461,1462],{"class":320}," pipefail\n",[301,1464,1465,1468,1471],{"class":126,"line":348},[301,1466,1467],{"class":324},"URL",[301,1469,1470],{"class":313},"=",[301,1472,1473],{"class":320},"\"https:\u002F\u002Fyour-domain.com\u002Fproducts\u002F42\"\n",[301,1475,1476],{"class":126,"line":361},[301,1477,1479],{"emptyLinePlaceholder":1478},true,"\n",[301,1481,1482],{"class":126,"line":367},[301,1483,1484],{"class":306},"# 1. First request after purge should MISS (proves the purge landed)\n",[301,1486,1487,1490,1492,1495,1497,1499,1502,1505,1507,1510,1512,1515,1518,1520,1523,1526,1529],{"class":126,"line":373},[301,1488,1489],{"class":324},"status",[301,1491,1470],{"class":313},[301,1493,1494],{"class":324},"$(",[301,1496,255],{"class":557},[301,1498,561],{"class":560},[301,1500,1501],{"class":320}," \"",[301,1503,1504],{"class":324},"$URL",[301,1506,786],{"class":320},[301,1508,1509],{"class":313}," |",[301,1511,575],{"class":557},[301,1513,1514],{"class":560}," -i",[301,1516,1517],{"class":320}," 'x-cache'",[301,1519,1509],{"class":313},[301,1521,1522],{"class":557}," tr",[301,1524,1525],{"class":560}," -d",[301,1527,1528],{"class":320}," '\\r'",[301,1530,1531],{"class":324},")\n",[301,1533,1534,1537,1540,1543],{"class":126,"line":379},[301,1535,1536],{"class":560},"echo",[301,1538,1539],{"class":320}," \"post-purge: ",[301,1541,1542],{"class":324},"$status",[301,1544,1545],{"class":320},"\"\n",[301,1547,1548,1550,1552,1554,1556,1558,1560,1563,1566,1569,1572,1574,1577,1580,1583,1586],{"class":126,"line":388},[301,1549,1536],{"class":560},[301,1551,1501],{"class":320},[301,1553,1542],{"class":324},[301,1555,786],{"class":320},[301,1557,1509],{"class":313},[301,1559,575],{"class":557},[301,1561,1562],{"class":560}," -qiE",[301,1564,1565],{"class":320}," 'miss|expired'",[301,1567,1568],{"class":313}," ||",[301,1570,1571],{"class":324}," { ",[301,1573,1536],{"class":560},[301,1575,1576],{"class":320}," \"FAIL: object still cached, purge did not land\"",[301,1578,1579],{"class":324},"; ",[301,1581,1582],{"class":560},"exit",[301,1584,1585],{"class":560}," 1",[301,1587,1588],{"class":324},"; }\n",[301,1590,1591],{"class":126,"line":491},[301,1592,1479],{"emptyLinePlaceholder":1478},[301,1594,1595],{"class":126,"line":1338},[301,1596,1597],{"class":306},"# 2. Second request should HIT within a second (proves edge re-caches, no per-request origin load)\n",[301,1599,1600,1603],{"class":126,"line":1360},[301,1601,1602],{"class":557},"sleep",[301,1604,1605],{"class":560}," 1\n",[301,1607,1608,1610,1612,1614,1616,1618,1620,1622,1624,1626,1628,1630,1632,1635,1637,1639,1641,1644,1646,1648,1650],{"class":126,"line":1366},[301,1609,255],{"class":557},[301,1611,561],{"class":560},[301,1613,1501],{"class":320},[301,1615,1504],{"class":324},[301,1617,786],{"class":320},[301,1619,1509],{"class":313},[301,1621,575],{"class":557},[301,1623,1514],{"class":560},[301,1625,1517],{"class":320},[301,1627,1509],{"class":313},[301,1629,575],{"class":557},[301,1631,1562],{"class":560},[301,1633,1634],{"class":320}," 'hit'",[301,1636,1568],{"class":313},[301,1638,1571],{"class":324},[301,1640,1536],{"class":560},[301,1642,1643],{"class":320}," \"FAIL: edge not re-caching after purge\"",[301,1645,1579],{"class":324},[301,1647,1582],{"class":560},[301,1649,1585],{"class":560},[301,1651,1588],{"class":324},[301,1653,1654],{"class":126,"line":1372},[301,1655,1656],{"class":306},"# trade-off: this asserts correctness on ONE representative URL. Do NOT\n",[301,1658,1659],{"class":126,"line":1378},[301,1660,1661],{"class":306},"# assert it on hundreds in CI — that itself becomes a load test. Sample\n",[301,1663,1664],{"class":126,"line":1384},[301,1665,1666],{"class":306},"# one URL per tag class and trust tag semantics for the rest.\n",[14,1668,1669,1670,1672,1673,1675,1676,1678],{},"Set explicit budgets and alert on regressions: edge hit ratio ",[27,1671,33],{}," sustained, cached TTFB ",[27,1674,37],{},", post-deploy origin RPS within origin headroom, and purge-to-fresh propagation ",[27,1677,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,1680,1682],{"id":1681},"related","Related",[185,1684,1685,1693,1698,1703,1709],{},[188,1686,1687,1689,1690,1692],{},[18,1688,519],{"href":518}," — set cache keys, ",[27,1691,661],{}," axes, and TTLs so tags and surrogate keys resolve to the objects you expect.",[188,1694,1695,1697],{},[18,1696,933],{"href":932}," — the stale-serving window that turns a soft purge into a smooth refill.",[188,1699,1700,1702],{},[18,1701,1424],{"href":1423}," — invalidating the client tier that edge purges can never reach.",[188,1704,1705,1708],{},[18,1706,1707],{"href":1428},"Purging CDN cache by tag on deploy"," — the deploy-time runbook for over- and under-purging.",[188,1710,1711,1714],{},[18,1712,1713],{"href":729},"Invalidating immutable hashed assets safely"," — why omission beats purging for content-hashed bundles, and how a reused filename resurrects a stale byte range.",[1716,1717,1719],"script",{"type":1718},"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-07-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",[1721,1722,1723],"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 .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}html pre.shiki code .ssM3C, html code.shiki .ssM3C{--shiki-default:#622CBC;--shiki-dark:#622CBC;--shiki-light:#622CBC}",{"title":297,"searchDepth":310,"depth":310,"links":1725},[1726,1727,1728,1729,1730,1731,1732,1736,1737],{"id":179,"depth":310,"text":180},{"id":269,"depth":310,"text":270},{"id":536,"depth":310,"text":537},{"id":612,"depth":310,"text":613},{"id":823,"depth":310,"text":824},{"id":936,"depth":310,"text":937},{"id":976,"depth":310,"text":977,"children":1733},[1734,1735],{"id":1116,"depth":328,"text":1117},{"id":1154,"depth":328,"text":1155},{"id":1432,"depth":310,"text":1433},{"id":1681,"depth":310,"text":1682},"A diagnostic workflow for invalidating CDN and edge caches without stale content or thundering-herd revalidation.","md",{"slug":12,"type":1741,"breadcrumb":1742,"datePublished":1748,"dateModified":1749},"topic",[1743,1745,1746],{"name":1744,"url":239},"Home",{"name":21,"url":20},{"name":5,"url":1747},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F","2026-06-18","2026-07-18","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns",{"title":1752,"description":1753},"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","YQmPhetjUGs-1HNxBfdign6KB_AEHG4ajh5_e0UcP3M",[1757,1760],{"title":21,"path":1758,"stem":1759,"children":-1},"\u002Fadvanced-caching-strategies-cdn-architecture","advanced-caching-strategies-cdn-architecture\u002Findex",{"title":1713,"path":1761,"stem":1762,"children":-1},"\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",1784398533901]