[{"data":1,"prerenderedAt":1154},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F":1147},{"id":4,"title":5,"body":6,"description":1129,"extension":1130,"meta":1131,"navigation":482,"path":1141,"seo":1142,"stem":1145,"__hash__":1146},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002Findex.md","Purging CDN cache by tag on deploy",{"type":7,"value":8,"toc":1112},"minimark",[9,13,28,40,150,155,158,237,241,246,257,261,275,279,291,295,301,305,308,312,319,413,419,423,426,574,580,657,668,672,679,852,860,864,877,882,886,889,1048,1064,1068,1103,1108],[10,11,5],"h1",{"id":12},"purging-cdn-cache-by-tag-on-deploy",[14,15,16,17,22,23,27],"p",{},"This scenario belongs to ",[18,19,21],"a",{"href":20},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F","Cache Invalidation Patterns"," within ",[18,24,26],{"href":25},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",": your deploy needs to evict exactly the objects the release changed — no more, no less — from a CI job.",[14,29,30,31,35,36,39],{},"The symptom that brings teams here is one of two opposites. Either a deploy ships but users keep seeing yesterday's content (under-purge), or every deploy hammers the origin with a synchronized miss storm and TTFB blows past ",[32,33,34],"code",{},"200ms"," for thirty seconds (over-purge). Both are tag-design failures, and both are fixable without resorting to ",[32,37,38],{},"purge everything"," on every release.",[14,41,42],{},[43,44,51,52,51,56,51,60,51,70,51,77,51,84,51,90,51,95,51,101,51,106,51,110,51,114,51,117,51,120,51,124,51,127,51,130,51,136,51,141,51,145,51],"svg",{"xmlns":45,"viewBox":46,"width":47,"role":48,"ariaLabel":49,"style":50},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 280","100%","img","A spectrum showing under-purge, correctly scoped tag purge, and over-purge with their symptoms","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:#001d3d"," ",[53,54,55],"title",{},"Purge blast radius spectrum",[57,58,59],"desc",{},"Under-purge leaves stale content, over-purge causes a miss storm; a tag scoped to the change sits between them.",[61,62],"rect",{"x":63,"y":63,"width":64,"height":65,"rx":66,"fill":67,"stroke":68,"style":69},"1","758","278","10","none","currentColor","stroke-opacity:0.18",[71,72,76],"text",{"x":73,"y":74,"fill":68,"style":75},"24","36","font-size:17px;font-weight:700","Match the tag to the change",[61,78],{"x":79,"y":80,"width":81,"height":80,"rx":82,"fill":68,"stroke":68,"style":83},"44","74","200","6","stroke-opacity:0.4;fill-opacity:0.06",[61,85],{"x":86,"y":80,"width":81,"height":80,"rx":82,"fill":87,"stroke":88,"style":89},"280","#ffc300","#b8860b","fill-opacity:0.22",[61,91],{"x":92,"y":80,"width":81,"height":80,"rx":82,"fill":93,"stroke":93,"style":94},"516","#0466c8","fill-opacity:0.14",[71,96,100],{"x":97,"y":98,"fill":68,"style":99},"144","100","font-size:13px;font-weight:700;text-anchor:middle","Under-purge",[71,102,105],{"x":97,"y":103,"fill":68,"style":104},"122","font-size:12px;text-anchor:middle","missing tags, 429s",[71,107,109],{"x":97,"y":108,"fill":68,"style":104},"140","stale content ships",[71,111,113],{"x":112,"y":98,"fill":68,"style":99},"380","Scoped tag purge",[71,115,116],{"x":112,"y":103,"fill":68,"style":104},"per-resource keys",[71,118,119],{"x":112,"y":108,"fill":68,"style":104},"evict only changed",[71,121,123],{"x":122,"y":98,"fill":68,"style":99},"616","Over-purge",[71,125,126],{"x":122,"y":103,"fill":68,"style":104},"coarse site tag",[71,128,129],{"x":122,"y":108,"fill":68,"style":104},"origin miss storm",[131,132],"line",{"x1":79,"y1":133,"x2":134,"y2":133,"stroke":68,"style":135},"178","716","stroke-opacity:0.3",[71,137,140],{"x":73,"y":138,"fill":68,"style":139},"208","font-size:12px","Verify: changed URL = MISS, untouched URL = HIT, hit ratio recovers >85%.",[71,142,144],{"x":73,"y":143,"fill":68,"style":139},"236","Compute changed tags from the build manifest; batch and rate-limit calls.",[71,146,149],{"x":73,"y":147,"fill":68,"style":148},"262","font-size:12px;fill-opacity:0.8","Reserve the build-rev key for emergencies, not every deploy.",[151,152,154],"h2",{"id":153},"rapid-diagnosis-is-the-deploy-over-or-under-purging","Rapid Diagnosis: Is the Deploy Over- or Under-Purging?",[14,156,157],{},"Run this checklist the moment a deploy looks wrong:",[159,160,161,180,207,213,223],"ul",{},[162,163,164,168,169,171,172,175,176,179],"li",{},[165,166,167],"strong",{},"Check the purge API response in CI logs."," A ",[32,170,81],{},"\u002F",[32,173,174],{},"201"," with the expected key list means the call landed. A ",[32,177,178],{},"429"," means you were rate-limited and the purge silently dropped — classic under-purge.",[162,181,182,51,185,188,189,192,193,171,196,199,200,203,204,206],{},[165,183,184],{},"Read the edge status header on a changed URL right after deploy.",[32,186,187],{},"curl -sI \u003Curl> | grep -i x-cache"," (or ",[32,190,191],{},"cf-cache-status","). It should show ",[32,194,195],{},"MISS",[32,197,198],{},"EXPIRED"," once, then ",[32,201,202],{},"HIT",". A persistent ",[32,205,202],{}," on changed content = under-purge.",[162,208,209,212],{},[165,210,211],{},"Watch origin RPS in the 60s after deploy."," A vertical spike to the entire steady-state cache miss volume = over-purge (you evicted far more than the release touched).",[162,214,215,218,219,222],{},[165,216,217],{},"Diff the tags you purged against the tags the build changed."," If you purged ",[32,220,221],{},"layout-v3"," for a one-product price change, you over-purged the whole site.",[162,224,225,228,229,232,233,236],{},[165,226,227],{},"Target thresholds:"," post-deploy origin RPS within origin headroom, edge hit ratio recovering to ",[32,230,231],{},">85%"," within minutes, propagation ",[32,234,235],{},"\u003C2s",".",[151,238,240],{"id":239},"root-cause-analysis","Root Cause Analysis",[242,243,245],"h3",{"id":244},"_1-over-broad-tags-evict-the-whole-catalog","1. Over-broad tags evict the whole catalog",[14,247,248,249,252,253,256],{},"A single shared tag like ",[32,250,251],{},"site"," or ",[32,254,255],{},"layout"," attached to every response means any deploy that purges it drops the entire edge cache. The blast radius is the union of every URL carrying the tag. This is over-purge by design: the tag granularity is coarser than the change.",[242,258,260],{"id":259},"_2-missing-or-stale-tags-leave-changed-content-cached","2. Missing or stale tags leave changed content cached",[14,262,263,264,171,267,270,271,274],{},"If the origin never stamped a ",[32,265,266],{},"Surrogate-Key",[32,268,269],{},"Cache-Tag"," on a response, no purge can target it. Purging ",[32,272,273],{},"product-42"," evicts nothing because no object carries that key. The deploy reports success and the content stays stale — under-purge from absent tagging.",[242,276,278],{"id":277},"_3-variant-fragmentation-one-logical-page-many-cache-keys","3. Variant fragmentation — one logical page, many cache keys",[14,280,281,282,285,286,290],{},"A page cached under multiple keys (",[32,283,284],{},"Vary: Accept-Encoding",", A\u002FB cookie, device class, query-string permutations) has several edge objects. A purge-by-URL hits one; the others survive. Tag purge solves this only if ",[287,288,289],"em",{},"every"," variant carries the tag.",[242,292,294],{"id":293},"_4-rate-limited-or-unbatched-purge-calls-drop-silently","4. Rate-limited or unbatched purge calls drop silently",[14,296,297,298,300],{},"Firing one purge request per changed URL across hundreds of assets hits the CDN's per-second purge limit. The CDN returns ",[32,299,178],{}," and the excess purges never execute. Under high deploy frequency this looks intermittent and is brutal to debug.",[151,302,304],{"id":303},"step-by-step-resolution","Step-by-Step Resolution",[14,306,307],{},"Fixes are ordered by impact: fix tag granularity first (root cause of over-purge), then guarantee tagging (root cause of under-purge), then batch the API calls.",[242,309,311],{"id":310},"step-1-tag-responses-at-the-right-granularity","Step 1 — Tag responses at the right granularity",[14,313,314,315,318],{},"Stamp keys that match the ",[287,316,317],{},"smallest unit you'll ever invalidate",", plus a few coarse keys for intentional broad purges.",[320,321,326],"pre",{"className":322,"code":323,"language":324,"meta":325,"style":325},"language-nginx shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","# Per-resource keys + one deliberate coarse key for full-section purges\nlocation ~ ^\u002Fproducts\u002F(\\d+)$ {\n  add_header Surrogate-Key \"product-$1 section-catalog build-$BUILD_REV\";\n  proxy_pass http:\u002F\u002Fbackend;\n}\n# trade-off: build-$BUILD_REV lets you purge an entire release in one call,\n# but if you purge it on EVERY deploy you've reinvented purge-everything.\n# Reserve the build key for emergencies; use product-N keys for normal deploys.\n","nginx","",[32,327,328,336,354,380,389,395,401,407],{"__ignoreMap":325},[329,330,332],"span",{"class":131,"line":331},1,[329,333,335],{"class":334},"sIIH1","# Per-resource keys + one deliberate coarse key for full-section purges\n",[329,337,339,343,346,350],{"class":131,"line":338},2,[329,340,342],{"class":341},"sP5qI","location",[329,344,345],{"class":341}," ~",[329,347,349],{"class":348},"s-_DF"," ^\u002Fproducts\u002F(\\d+)$ ",[329,351,353],{"class":352},"syybb","{\n",[329,355,357,360,363,366,368,371,374,377],{"class":131,"line":356},3,[329,358,359],{"class":341},"  add_header ",[329,361,362],{"class":352},"Surrogate-Key ",[329,364,365],{"class":348},"\"product-$",[329,367,63],{"class":352},[329,369,370],{"class":348}," section-catalog build-$",[329,372,373],{"class":352},"BUILD_REV",[329,375,376],{"class":348},"\"",[329,378,379],{"class":352},";\n",[329,381,383,386],{"class":131,"line":382},4,[329,384,385],{"class":341},"  proxy_pass ",[329,387,388],{"class":352},"http:\u002F\u002Fbackend;\n",[329,390,392],{"class":131,"line":391},5,[329,393,394],{"class":352},"}\n",[329,396,398],{"class":131,"line":397},6,[329,399,400],{"class":334},"# trade-off: build-$BUILD_REV lets you purge an entire release in one call,\n",[329,402,404],{"class":131,"line":403},7,[329,405,406],{"class":334},"# but if you purge it on EVERY deploy you've reinvented purge-everything.\n",[329,408,410],{"class":131,"line":409},8,[329,411,412],{"class":334},"# Reserve the build key for emergencies; use product-N keys for normal deploys.\n",[14,414,415,418],{},[165,416,417],{},"Expected outcome:"," a routine single-product deploy now evicts one object instead of the whole catalog, holding origin RPS flat (over-purge eliminated).",[242,420,422],{"id":421},"step-2-compute-the-affected-tags-from-the-build-then-purge-only-those","Step 2 — Compute the affected tags from the build, then purge only those",[14,424,425],{},"Derive the changed tags from the build manifest or git diff so the purge is scoped to what actually changed.",[320,427,431],{"className":428,"code":429,"language":430,"meta":325,"style":325},"language-bash shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","#!\u002Fusr\u002Fbin\u002Fenv bash\n# ci\u002Fpurge-fastly.sh — purge only the surrogate keys this deploy changed\nset -euo pipefail\n# CHANGED_KEYS is computed from `git diff --name-only` mapped to content ids\nCHANGED_KEYS=\"$1\"   # e.g. \"product-42 product-77 section-catalog\"\n\ncurl -fsS -X POST \"https:\u002F\u002Fapi.fastly.com\u002Fservice\u002F$FASTLY_SERVICE_ID\u002Fpurge\" \\\n  -H \"Fastly-Key: $FASTLY_PURGE_TOKEN\" \\\n  -H \"Fastly-Soft-Purge: 1\" \\\n  -H \"surrogate-key: $CHANGED_KEYS\"\n# trade-off: soft purge serves one stale response per object while it\n# revalidates, smoothing origin load. Do NOT soft-purge a security or\n# pricing fix where a single stale hit is unacceptable — drop the\n# Fastly-Soft-Purge header to force a hard purge there.\n","bash",[32,432,433,438,443,455,460,478,484,511,526,536,550,556,562,568],{"__ignoreMap":325},[329,434,435],{"class":131,"line":331},[329,436,437],{"class":334},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[329,439,440],{"class":131,"line":338},[329,441,442],{"class":334},"# ci\u002Fpurge-fastly.sh — purge only the surrogate keys this deploy changed\n",[329,444,445,449,452],{"class":131,"line":356},[329,446,448],{"class":447},"sf6mN","set",[329,450,451],{"class":447}," -euo",[329,453,454],{"class":348}," pipefail\n",[329,456,457],{"class":131,"line":382},[329,458,459],{"class":334},"# CHANGED_KEYS is computed from `git diff --name-only` mapped to content ids\n",[329,461,462,465,468,470,473,475],{"class":131,"line":391},[329,463,464],{"class":352},"CHANGED_KEYS",[329,466,467],{"class":341},"=",[329,469,376],{"class":348},[329,471,472],{"class":447},"$1",[329,474,376],{"class":348},[329,476,477],{"class":334},"   # e.g. \"product-42 product-77 section-catalog\"\n",[329,479,480],{"class":131,"line":397},[329,481,483],{"emptyLinePlaceholder":482},true,"\n",[329,485,486,490,493,496,499,502,505,508],{"class":131,"line":403},[329,487,489],{"class":488},"seIZK","curl",[329,491,492],{"class":447}," -fsS",[329,494,495],{"class":447}," -X",[329,497,498],{"class":348}," POST",[329,500,501],{"class":348}," \"https:\u002F\u002Fapi.fastly.com\u002Fservice\u002F",[329,503,504],{"class":352},"$FASTLY_SERVICE_ID",[329,506,507],{"class":348},"\u002Fpurge\"",[329,509,510],{"class":341}," \\\n",[329,512,513,516,519,522,524],{"class":131,"line":409},[329,514,515],{"class":447},"  -H",[329,517,518],{"class":348}," \"Fastly-Key: ",[329,520,521],{"class":352},"$FASTLY_PURGE_TOKEN",[329,523,376],{"class":348},[329,525,510],{"class":341},[329,527,529,531,534],{"class":131,"line":528},9,[329,530,515],{"class":447},[329,532,533],{"class":348}," \"Fastly-Soft-Purge: 1\"",[329,535,510],{"class":341},[329,537,539,541,544,547],{"class":131,"line":538},10,[329,540,515],{"class":447},[329,542,543],{"class":348}," \"surrogate-key: ",[329,545,546],{"class":352},"$CHANGED_KEYS",[329,548,549],{"class":348},"\"\n",[329,551,553],{"class":131,"line":552},11,[329,554,555],{"class":334},"# trade-off: soft purge serves one stale response per object while it\n",[329,557,559],{"class":131,"line":558},12,[329,560,561],{"class":334},"# revalidates, smoothing origin load. Do NOT soft-purge a security or\n",[329,563,565],{"class":131,"line":564},13,[329,566,567],{"class":334},"# pricing fix where a single stale hit is unacceptable — drop the\n",[329,569,571],{"class":131,"line":570},14,[329,572,573],{"class":334},"# Fastly-Soft-Purge header to force a hard purge there.\n",[14,575,576,577,579],{},"The Cloudflare Enterprise equivalent purges by ",[32,578,269],{},":",[320,581,583],{"className":428,"code":582,"language":430,"meta":325,"style":325},"# ci\u002Fpurge-cloudflare.sh — tag purge, max 30 tags per request\ncurl -fsS -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\",\"product-77\",\"section-catalog\"]}'\n# trade-off: Cloudflare caps purge_cache at 30 tags per call, so large\n# deploys must chunk the tag list. Don't fall back to {\"purge_everything\":true}\n# when you exceed 30 — batch the tags (Step 3) to keep the blast radius small.\n",[32,584,585,590,611,625,634,642,647,652],{"__ignoreMap":325},[329,586,587],{"class":131,"line":331},[329,588,589],{"class":334},"# ci\u002Fpurge-cloudflare.sh — tag purge, max 30 tags per request\n",[329,591,592,594,596,598,600,603,606,609],{"class":131,"line":338},[329,593,489],{"class":488},[329,595,492],{"class":447},[329,597,495],{"class":447},[329,599,498],{"class":348},[329,601,602],{"class":348}," \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F",[329,604,605],{"class":352},"$CF_ZONE_ID",[329,607,608],{"class":348},"\u002Fpurge_cache\"",[329,610,510],{"class":341},[329,612,613,615,618,621,623],{"class":131,"line":356},[329,614,515],{"class":447},[329,616,617],{"class":348}," \"Authorization: Bearer ",[329,619,620],{"class":352},"$CF_PURGE_TOKEN",[329,622,376],{"class":348},[329,624,510],{"class":341},[329,626,627,629,632],{"class":131,"line":382},[329,628,515],{"class":447},[329,630,631],{"class":348}," \"Content-Type: application\u002Fjson\"",[329,633,510],{"class":341},[329,635,636,639],{"class":131,"line":391},[329,637,638],{"class":447},"  --data",[329,640,641],{"class":348}," '{\"tags\":[\"product-42\",\"product-77\",\"section-catalog\"]}'\n",[329,643,644],{"class":131,"line":397},[329,645,646],{"class":334},"# trade-off: Cloudflare caps purge_cache at 30 tags per call, so large\n",[329,648,649],{"class":131,"line":403},[329,650,651],{"class":334},"# deploys must chunk the tag list. Don't fall back to {\"purge_everything\":true}\n",[329,653,654],{"class":131,"line":409},[329,655,656],{"class":334},"# when you exceed 30 — batch the tags (Step 3) to keep the blast radius small.\n",[14,658,659,661,662,664,665,667],{},[165,660,417],{}," the deploy evicts exactly the changed content; previously stale pages now show ",[32,663,195],{}," then ",[32,666,202],{},", closing the under-purge gap.",[242,669,671],{"id":670},"step-3-batch-and-rate-limit-the-purge-calls","Step 3 — Batch and rate-limit the purge calls",[14,673,674,675,678],{},"Group keys into a single request (Fastly accepts many keys in one ",[32,676,677],{},"surrogate-key"," header; Cloudflare allows 30 tags per call) and chunk anything larger.",[320,680,682],{"className":428,"code":681,"language":430,"meta":325,"style":325},"# Chunk a long tag list into 30-tag batches for Cloudflare\nprintf '%s\\n' $CHANGED_KEYS | xargs -n 30 | while read -r batch; do\n  tags=$(printf '\"%s\",' $batch | sed 's\u002F,$\u002F\u002F')\n  curl -fsS -X POST \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F$CF_ZONE_ID\u002Fpurge_cache\" \\\n    -H \"Authorization: Bearer $CF_PURGE_TOKEN\" -H \"Content-Type: application\u002Fjson\" \\\n    --data \"{\\\"tags\\\":[$tags]}\"\n  sleep 1\ndone\n# trade-off: the 1s pause keeps you under the purge rate limit but adds\n# latency to large deploys. Drop it for small tag sets; keep it when a\n# release can touch hundreds of tags, or the 429s return.\n",[32,683,684,689,733,762,781,799,824,832,837,842,847],{"__ignoreMap":325},[329,685,686],{"class":131,"line":331},[329,687,688],{"class":334},"# Chunk a long tag list into 30-tag batches for Cloudflare\n",[329,690,691,694,697,700,703,706,709,712,715,718,721,724,727,730],{"class":131,"line":338},[329,692,693],{"class":447},"printf",[329,695,696],{"class":348}," '%s\\n'",[329,698,699],{"class":352}," $CHANGED_KEYS ",[329,701,702],{"class":341},"|",[329,704,705],{"class":488}," xargs",[329,707,708],{"class":447}," -n",[329,710,711],{"class":447}," 30",[329,713,714],{"class":341}," |",[329,716,717],{"class":341}," while",[329,719,720],{"class":447}," read",[329,722,723],{"class":447}," -r",[329,725,726],{"class":348}," batch",[329,728,729],{"class":352},"; ",[329,731,732],{"class":341},"do\n",[329,734,735,738,740,743,745,748,751,753,756,759],{"class":131,"line":356},[329,736,737],{"class":352},"  tags",[329,739,467],{"class":341},[329,741,742],{"class":352},"$(",[329,744,693],{"class":447},[329,746,747],{"class":348}," '\"%s\",'",[329,749,750],{"class":352}," $batch ",[329,752,702],{"class":341},[329,754,755],{"class":488}," sed",[329,757,758],{"class":348}," 's\u002F,$\u002F\u002F'",[329,760,761],{"class":352},")\n",[329,763,764,767,769,771,773,775,777,779],{"class":131,"line":382},[329,765,766],{"class":488},"  curl",[329,768,492],{"class":447},[329,770,495],{"class":447},[329,772,498],{"class":348},[329,774,602],{"class":348},[329,776,605],{"class":352},[329,778,608],{"class":348},[329,780,510],{"class":341},[329,782,783,786,788,790,792,795,797],{"class":131,"line":391},[329,784,785],{"class":447},"    -H",[329,787,617],{"class":348},[329,789,620],{"class":352},[329,791,376],{"class":348},[329,793,794],{"class":447}," -H",[329,796,631],{"class":348},[329,798,510],{"class":341},[329,800,801,804,807,810,813,815,818,821],{"class":131,"line":397},[329,802,803],{"class":447},"    --data",[329,805,806],{"class":348}," \"{",[329,808,809],{"class":341},"\\\"",[329,811,812],{"class":348},"tags",[329,814,809],{"class":341},[329,816,817],{"class":348},":[",[329,819,820],{"class":352},"$tags",[329,822,823],{"class":348},"]}\"\n",[329,825,826,829],{"class":131,"line":403},[329,827,828],{"class":488},"  sleep",[329,830,831],{"class":447}," 1\n",[329,833,834],{"class":131,"line":409},[329,835,836],{"class":341},"done\n",[329,838,839],{"class":131,"line":528},[329,840,841],{"class":334},"# trade-off: the 1s pause keeps you under the purge rate limit but adds\n",[329,843,844],{"class":131,"line":538},[329,845,846],{"class":334},"# latency to large deploys. Drop it for small tag sets; keep it when a\n",[329,848,849],{"class":131,"line":552},[329,850,851],{"class":334},"# release can touch hundreds of tags, or the 429s return.\n",[14,853,854,856,857,859],{},[165,855,417],{}," no more silent ",[32,858,178],{}," drops; every intended key is evicted even on large deploys (under-purge from rate limiting eliminated).",[242,861,863],{"id":862},"step-4-guarantee-every-variant-carries-the-tag","Step 4 — Guarantee every variant carries the tag",[14,865,866,867,869,870,873,874,236],{},"Apply the ",[32,868,266],{}," before any ",[32,871,872],{},"Vary","-splitting or A\u002FB logic so all variants of a page inherit the same tag, making tag purge cover what URL purge cannot. For CloudFront, which has no native tagging, fall back to path-pattern invalidations scoped to the changed prefixes rather than ",[32,875,876],{},"\u002F*",[14,878,879,881],{},[165,880,417],{}," every cached variant of a changed page is evicted by a single tag purge; no surviving stale variant.",[151,883,885],{"id":884},"verification","Verification",[14,887,888],{},"Confirm the deploy purged correctly with a before\u002Fafter check wired into CI:",[320,890,892],{"className":428,"code":891,"language":430,"meta":325,"style":325},"# Assert a changed URL is fresh, an unchanged URL is still warm\nchanged=\"https:\u002F\u002Fyour-domain.com\u002Fproducts\u002F42\"\nuntouched=\"https:\u002F\u002Fyour-domain.com\u002Fabout\u002F\"\n\ncurl -sI \"$changed\"  | grep -i x-cache | grep -qiE 'miss|expired' \\\n  || { echo \"FAIL: changed page still cached (under-purge)\"; exit 1; }\ncurl -sI \"$untouched\" | grep -i x-cache | grep -qiE 'hit' \\\n  || { echo \"FAIL: unrelated page evicted (over-purge)\"; exit 1; }\n# trade-off: checks one URL per class. Sample one per tag, not all URLs —\n# verifying hundreds in CI becomes its own load test.\n",[32,893,894,899,909,919,923,962,987,1019,1038,1043],{"__ignoreMap":325},[329,895,896],{"class":131,"line":331},[329,897,898],{"class":334},"# Assert a changed URL is fresh, an unchanged URL is still warm\n",[329,900,901,904,906],{"class":131,"line":338},[329,902,903],{"class":352},"changed",[329,905,467],{"class":341},[329,907,908],{"class":348},"\"https:\u002F\u002Fyour-domain.com\u002Fproducts\u002F42\"\n",[329,910,911,914,916],{"class":131,"line":356},[329,912,913],{"class":352},"untouched",[329,915,467],{"class":341},[329,917,918],{"class":348},"\"https:\u002F\u002Fyour-domain.com\u002Fabout\u002F\"\n",[329,920,921],{"class":131,"line":382},[329,922,483],{"emptyLinePlaceholder":482},[329,924,925,927,930,933,936,938,941,944,947,950,952,954,957,960],{"class":131,"line":391},[329,926,489],{"class":488},[329,928,929],{"class":447}," -sI",[329,931,932],{"class":348}," \"",[329,934,935],{"class":352},"$changed",[329,937,376],{"class":348},[329,939,940],{"class":341},"  |",[329,942,943],{"class":488}," grep",[329,945,946],{"class":447}," -i",[329,948,949],{"class":348}," x-cache",[329,951,714],{"class":341},[329,953,943],{"class":488},[329,955,956],{"class":447}," -qiE",[329,958,959],{"class":348}," 'miss|expired'",[329,961,510],{"class":341},[329,963,964,967,970,973,976,978,981,984],{"class":131,"line":397},[329,965,966],{"class":341},"  ||",[329,968,969],{"class":352}," { ",[329,971,972],{"class":447},"echo",[329,974,975],{"class":348}," \"FAIL: changed page still cached (under-purge)\"",[329,977,729],{"class":352},[329,979,980],{"class":447},"exit",[329,982,983],{"class":447}," 1",[329,985,986],{"class":352},"; }\n",[329,988,989,991,993,995,998,1000,1002,1004,1006,1008,1010,1012,1014,1017],{"class":131,"line":403},[329,990,489],{"class":488},[329,992,929],{"class":447},[329,994,932],{"class":348},[329,996,997],{"class":352},"$untouched",[329,999,376],{"class":348},[329,1001,714],{"class":341},[329,1003,943],{"class":488},[329,1005,946],{"class":447},[329,1007,949],{"class":348},[329,1009,714],{"class":341},[329,1011,943],{"class":488},[329,1013,956],{"class":447},[329,1015,1016],{"class":348}," 'hit'",[329,1018,510],{"class":341},[329,1020,1021,1023,1025,1027,1030,1032,1034,1036],{"class":131,"line":409},[329,1022,966],{"class":341},[329,1024,969],{"class":352},[329,1026,972],{"class":447},[329,1028,1029],{"class":348}," \"FAIL: unrelated page evicted (over-purge)\"",[329,1031,729],{"class":352},[329,1033,980],{"class":447},[329,1035,983],{"class":447},[329,1037,986],{"class":352},[329,1039,1040],{"class":131,"line":528},[329,1041,1042],{"class":334},"# trade-off: checks one URL per class. Sample one per tag, not all URLs —\n",[329,1044,1045],{"class":131,"line":538},[329,1046,1047],{"class":334},"# verifying hundreds in CI becomes its own load test.\n",[14,1049,1050,1051,1054,1055,1058,1059,1063],{},"The combined assertion is the tell: the changed URL must miss, the untouched URL must still hit. If both pass, the purge was correctly scoped. In production, confirm via CDN analytics that the post-deploy edge hit ratio dips only slightly and recovers above ",[32,1052,1053],{},"85%"," within minutes, and that origin RPS never exceeded its ",[32,1056,1057],{},"TTFB ≤ 200ms"," headroom. Tie soft purge to a stale window per ",[18,1060,1062],{"href":1061},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002F","Stale-While-Revalidate Implementation"," to keep the refill smooth.",[151,1065,1067],{"id":1066},"related","Related",[159,1069,1070,1075,1082,1089,1096],{},[162,1071,1072,1074],{},[18,1073,21],{"href":20}," — the four purge primitives and when each applies.",[162,1076,1077,1081],{},[18,1078,1080],{"href":1079},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002F","CDN Edge Caching Configuration"," — aligning cache keys so tags resolve to the objects you expect.",[162,1083,1084,1088],{},[18,1085,1087],{"href":1086},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F","Invalidating immutable hashed assets safely"," — why hashed bundles need omission, not tag purge.",[162,1090,1091,1095],{},[18,1092,1094],{"href":1093},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fhttp-cache-control-headers-explained\u002Fsetting-up-immutable-cache-headers-for-hashed-assets\u002F","Setting up immutable cache headers for hashed assets"," — the header config that makes invalidation-by-omission work.",[162,1097,1098,1102],{},[18,1099,1101],{"href":1100},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002F","Service Worker Caching Strategies"," — invalidating the client tier a CDN purge can't reach.",[1104,1105,1107],"script",{"type":1106},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@graph\": [\n    {\n      \"@type\": \"TechArticle\",\n      \"headline\": \"Purging CDN cache by tag on deploy\",\n      \"description\": \"Fix over- and under-purging by evicting only the surrogate keys a release changed, using Fastly and Cloudflare purge APIs in CI.\",\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\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F\"\n    },\n    {\n      \"@type\": \"HowTo\",\n      \"name\": \"Purge only the affected CDN assets by cache tag during a deploy\",\n      \"step\": [\n        { \"@type\": \"HowToStep\", \"name\": \"Tag responses at the right granularity\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F#step-1--tag-responses-at-the-right-granularity\" },\n        { \"@type\": \"HowToStep\", \"name\": \"Compute affected tags and purge only those\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F#step-2--compute-the-affected-tags-from-the-build-then-purge-only-those\" },\n        { \"@type\": \"HowToStep\", \"name\": \"Batch and rate-limit the purge calls\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F#step-3--batch-and-rate-limit-the-purge-calls\" },\n        { \"@type\": \"HowToStep\", \"name\": \"Guarantee every variant carries the tag\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F#step-4--guarantee-every-variant-carries-the-tag\" }\n      ]\n    },\n    {\n      \"@type\": \"BreadcrumbList\",\n      \"itemListElement\": [\n        { \"@type\": \"ListItem\", \"position\": 1, \"name\": \"Home\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\" },\n        { \"@type\": \"ListItem\", \"position\": 2, \"name\": \"Advanced Caching Strategies & CDN Architecture\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002F\" },\n        { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"Cache Invalidation Patterns\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F\" },\n        { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Purging CDN cache by tag on deploy\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F\" }\n      ]\n    }\n  ]\n}\n",[1109,1110,1111],"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 .sf6mN, html code.shiki .sf6mN{--shiki-default:#023B95;--shiki-dark:#023B95;--shiki-light:#023B95}html pre.shiki code .seIZK, html code.shiki .seIZK{--shiki-default:#702C00;--shiki-dark:#702C00;--shiki-light:#702C00}",{"title":325,"searchDepth":338,"depth":338,"links":1113},[1114,1115,1121,1127,1128],{"id":153,"depth":338,"text":154},{"id":239,"depth":338,"text":240,"children":1116},[1117,1118,1119,1120],{"id":244,"depth":356,"text":245},{"id":259,"depth":356,"text":260},{"id":277,"depth":356,"text":278},{"id":293,"depth":356,"text":294},{"id":303,"depth":338,"text":304,"children":1122},[1123,1124,1125,1126],{"id":310,"depth":356,"text":311},{"id":421,"depth":356,"text":422},{"id":670,"depth":356,"text":671},{"id":862,"depth":356,"text":863},{"id":884,"depth":338,"text":885},{"id":1066,"depth":338,"text":1067},"A deploy-time runbook for evicting only the assets a release touched, using Fastly and Cloudflare surrogate-key purge APIs.","md",{"slug":12,"type":1132,"breadcrumb":1133,"datePublished":1140,"dateModified":1140},"long_tail",[1134,1136,1137,1138],{"name":1135,"url":171},"Home",{"name":26,"url":25},{"name":21,"url":20},{"name":5,"url":1139},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F","2026-06-18","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy",{"title":1143,"description":1144},"Purging CDN Cache by Tag on Deploy","Automatically purge only the assets affected by a deploy using cache tags and surrogate keys. Fix over-purging stampedes and under-purging stale content in CI.","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002Findex","RZvZ3bDetsK7C-1fkVOJ7hi6pPVd5HjFQs7sSv-qCFA",[1148,1151],{"title":1087,"path":1149,"stem":1150,"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",{"title":1080,"path":1152,"stem":1153,"children":-1},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration","advanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Findex",1782237170939]