[{"data":1,"prerenderedAt":1119},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fversioning-api-responses-for-cache-busting":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fversioning-api-responses-for-cache-busting":1110},{"id":4,"title":5,"body":6,"description":1089,"extension":1090,"meta":1091,"navigation":1103,"path":1104,"seo":1105,"stem":1108,"__hash__":1109},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fversioning-api-responses-for-cache-busting\u002Findex.md","Versioning API Responses for Cache Busting",{"type":7,"value":8,"toc":1071},"minimark",[9,14,29,49,203,208,245,249,255,261,270,278,363,367,372,568,572,734,738,752,756,759,875,879,882,886,893,897,900,904,910,914,940,944,950,956,962,968,972,984,993,1002,1011,1020,1029,1033,1056,1061,1064,1067],[10,11,13],"h1",{"id":12},"how-to-version-api-responses-for-cache-busting","How to Version API Responses for Cache Busting",[15,16,17,18,23,24,28],"p",{},"This guide applies the hashed-asset idea to data, within ",[19,20,22],"a",{"href":21},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F","Cache Invalidation Patterns"," in ",[19,25,27],{"href":26},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",". Static assets solved invalidation years ago: put a content hash in the filename, cache it forever, and change the reference when content changes. APIs usually do the opposite — stable URLs, short TTLs and purges — and live with the trade-off between freshness and hit rate.",[15,30,31,32,36,37,40,41,44,45,48],{},"Versioned API URLs bring the asset model to data. Instead of ",[33,34,35],"code",{},"\u002Fapi\u002Fcatalog",", clients request ",[33,38,39],{},"\u002Fapi\u002Fcatalog?v=8f3a"," (or ",[33,42,43],{},"\u002Fapi\u002Fcatalog\u002F8f3a","), where the version identifies the dataset's state. That URL's response never changes, so it can be cached with a year-long ",[33,46,47],{},"max-age"," at every layer. When data changes, the version changes, clients request a new URL, and old entries simply age out. No purge, no race between purge and refill, and no stale data for clients that know the current version.",[15,50,51],{},[52,53,59,60,59,67,59,71,59,74,59,92,59,100,59,106,59,114,59,121,59,126,59,129,59,133,59,136,59,142,59,146,59,150,59,154,59,158,59,161,59,165,59,168,59,172,59,175,59,179,59,182,59,191,59,195,59,199,59],"svg",{"viewBox":54,"width":55,"role":56,"ariaLabel":57,"style":58},"0 0 760 166","100%","img","Flow from a data change producing a new version, published to clients via HTML or a manifest, to cacheable versioned requests.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[61,62],"rect",{"className":63,"x":65,"y":65,"width":55,"height":55,"fill":66},[64],"svg-canvas","0","#ffffff",[68,69,70],"title",{},"Versioned data delivery",[72,73,57],"desc",{},[75,76,77],"defs",{},[78,79,86],"marker",{"id":80,"viewBox":81,"refX":82,"refY":83,"markerWidth":84,"markerHeight":84,"orient":85},"fac7f1b0f0","0 0 10 10","9","5","7","auto-start-reverse",[87,88],"path",{"d":89,"fill":90,"style":91},"M0 0 L10 5 L0 10 z","currentColor","fill-opacity:0.7",[61,93],{"x":94,"y":94,"width":95,"height":96,"rx":97,"fill":98,"stroke":90,"style":99},"1","758","164","10","none","stroke-opacity:0.18",[101,102,70],"text",{"x":103,"y":104,"fill":90,"style":105},"28.0","34.0","font-size:16px;font-weight:700",[61,107],{"x":103,"y":108,"width":109,"height":110,"rx":111,"fill":112,"stroke":112,"style":113},"56.0","108.8","88.0","6","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[101,115,120],{"x":116,"y":117,"fill":90,"style":118,"textAnchor":119},"82.4","78.0","font-size:13px;font-weight:700","middle","Data changes",[101,122,125],{"x":116,"y":123,"fill":90,"style":124,"textAnchor":119},"96.0","font-size:12px","publish \u002F import",[61,127],{"x":128,"y":108,"width":109,"height":110,"rx":111,"fill":112,"stroke":112,"style":113},"176.8",[101,130,132],{"x":131,"y":117,"fill":90,"style":118,"textAnchor":119},"231.2","New version",[101,134,135],{"x":131,"y":123,"fill":90,"style":124,"textAnchor":119},"hash or counter",[61,137],{"x":138,"y":108,"width":109,"height":110,"rx":111,"fill":139,"stroke":140,"style":141},"325.6","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[101,143,145],{"x":144,"y":117,"fill":90,"style":118,"textAnchor":119},"380.0","Version",[101,147,149],{"x":144,"y":148,"fill":90,"style":118,"textAnchor":119},"95.0","published",[101,151,153],{"x":144,"y":152,"fill":90,"style":124,"textAnchor":119},"113.0","in HTML or",[101,155,157],{"x":144,"y":156,"fill":90,"style":124,"textAnchor":119},"129.0","manifest",[61,159],{"x":160,"y":108,"width":109,"height":110,"rx":111,"fill":112,"stroke":112,"style":113},"474.4",[101,162,164],{"x":163,"y":117,"fill":90,"style":118,"textAnchor":119},"528.8","Client requests",[101,166,167],{"x":163,"y":123,"fill":90,"style":124,"textAnchor":119},"\u002Fapi\u002Fcatalog?v=8f",[101,169,171],{"x":163,"y":170,"fill":90,"style":124,"textAnchor":119},"112.0","3a",[61,173],{"x":174,"y":108,"width":109,"height":110,"rx":111,"fill":112,"stroke":112,"style":113},"623.2",[101,176,178],{"x":177,"y":117,"fill":90,"style":118,"textAnchor":119},"677.6","Cached forever",[101,180,181],{"x":177,"y":123,"fill":90,"style":124,"textAnchor":119},"immutable",[183,184],"line",{"x1":185,"y1":186,"x2":187,"y2":186,"stroke":90,"strokeWidth":188,"style":189,"markerEnd":190},"136.8","100.0","174.8","1.5","stroke-opacity:0.6","url(#fac7f1b0f0)",[183,192],{"x1":193,"y1":186,"x2":194,"y2":186,"stroke":90,"strokeWidth":188,"style":189,"markerEnd":190},"285.6","323.6",[183,196],{"x1":197,"y1":186,"x2":198,"y2":186,"stroke":90,"strokeWidth":188,"style":189,"markerEnd":190},"434.4","472.4",[183,200],{"x1":201,"y1":186,"x2":202,"y2":186,"stroke":90,"strokeWidth":188,"style":189,"markerEnd":190},"583.2","621.2",[204,205,207],"h2",{"id":206},"rapid-diagnosis","Rapid Diagnosis",[209,210,211,219,229,235],"ul",{},[212,213,214,218],"li",{},[215,216,217],"strong",{},"Identify read-heavy, change-light datasets."," Catalogues, navigation trees, translations, configuration and reference data are classic candidates.",[212,220,221,224,225,228],{},[215,222,223],{},"Check current caching."," Short TTLs or ",[33,226,227],{},"no-cache"," on these endpoints mean every page view pays a request.",[212,230,231,234],{},[215,232,233],{},"Check for purge complexity."," Many tags and purge calls per publish indicate a dataset that could be versioned instead.",[212,236,237,240,241,244],{},[215,238,239],{},"Find a natural version source."," A database ",[33,242,243],{},"updated_at",", a CMS publish ID, a build ID or a content hash.",[204,246,248],{"id":247},"root-cause-analysis","Root Cause Analysis",[15,250,251,254],{},[215,252,253],{},"1. Stable URLs force short TTLs."," Without a way to signal change, caches must expire quickly or risk staleness.",[15,256,257,260],{},[215,258,259],{},"2. Purges are eventually consistent."," Between a data change and the purge completing across all locations (and browsers, which cannot be purged), clients may see old data.",[15,262,263,266,267,269],{},[215,264,265],{},"3. Browser caches cannot be invalidated."," Any ",[33,268,47],{}," on a stable API URL is a promise you cannot retract.",[15,271,272,59,275,277],{},[215,273,274],{},"4. Revalidation costs a round trip.",[33,276,227],{}," with ETags is correct but still pays latency on every use.",[15,279,280],{},[52,281,59,284,59,287,59,290,59,292,59,295,59,297,59,301,59,307,59,312,59,317,59,320,59,323,59,326,59,329,59,332,59,335,59,338,59,342,59,344,59,348,59,350,59,353,59,355,59,358,59,360,59],{"viewBox":282,"width":55,"role":56,"ariaLabel":283,"style":58},"0 0 760 228","Comparison of caching a dataset at a stable URL with short TTLs versus at versioned URLs cached immutably.",[61,285],{"className":286,"x":65,"y":65,"width":55,"height":55,"fill":66},[64],[68,288,289],{},"Stable API URL vs versioned API URL",[72,291,283],{},[61,293],{"x":94,"y":94,"width":95,"height":294,"rx":97,"fill":98,"stroke":90,"style":99},"226",[101,296,289],{"x":103,"y":104,"fill":90,"style":105},[61,298],{"x":103,"y":108,"width":299,"height":300,"rx":111,"fill":139,"stroke":140,"style":141},"340.0","150.0",[101,302,306],{"x":303,"y":304,"fill":90,"style":305},"42.0","82.0","font-size:14px;font-weight:700","Stable URL, short TTL",[101,308,311],{"x":303,"y":309,"fill":90,"style":310},"108.0","font-size:12.5px;font-weight:700","•",[101,313,316],{"x":108,"y":309,"fill":90,"style":314,"textAnchor":315},"font-size:12.5px","start","\u002Fapi\u002Fcatalog with max-age=60",[101,318,311],{"x":303,"y":319,"fill":90,"style":310},"132.0",[101,321,322],{"x":108,"y":319,"fill":90,"style":314,"textAnchor":315},"Revalidation or refetch every minute",[101,324,311],{"x":303,"y":325,"fill":90,"style":310},"156.0",[101,327,328],{"x":108,"y":325,"fill":90,"style":314,"textAnchor":315},"Purges needed for prompt updates",[101,330,311],{"x":303,"y":331,"fill":90,"style":310},"180.0",[101,333,334],{"x":108,"y":331,"fill":90,"style":314,"textAnchor":315},"Browser may hold stale data",[61,336],{"x":337,"y":108,"width":299,"height":300,"rx":111,"fill":112,"stroke":112,"style":113},"392.0",[101,339,341],{"x":340,"y":304,"fill":90,"style":305},"406.0","Versioned URL, immutable",[101,343,311],{"x":340,"y":309,"fill":90,"style":310},[101,345,347],{"x":346,"y":309,"fill":90,"style":314,"textAnchor":315},"420.0","\u002Fapi\u002Fcatalog?v=8f3a, max-age=1y",[101,349,311],{"x":340,"y":319,"fill":90,"style":310},[101,351,352],{"x":346,"y":319,"fill":90,"style":314,"textAnchor":315},"Zero requests while version is current",[101,354,311],{"x":340,"y":325,"fill":90,"style":310},[101,356,357],{"x":346,"y":325,"fill":90,"style":314,"textAnchor":315},"New version = new URL, no purge",[101,359,311],{"x":340,"y":331,"fill":90,"style":310},[101,361,362],{"x":346,"y":331,"fill":90,"style":314,"textAnchor":315},"Clients with the new version never see stale data",[204,364,366],{"id":365},"step-by-step-resolution","Step-by-Step Resolution",[368,369,371],"h3",{"id":370},"_1-compute-a-version-for-the-dataset","1. Compute a version for the dataset",[373,374,379],"pre",{"className":375,"code":376,"language":377,"meta":378,"style":378},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F On publish: version = short hash of the dataset's last update and schema version.\nimport { createHash } from 'node:crypto';\nexport async function catalogVersion(db) {\n  const { max } = await db.one('SELECT max(updated_at) AS max FROM products');\n  return createHash('sha256').update(`${SCHEMA_VERSION}:${max.toISOString()}`).digest('hex').slice(0, 8);\n}\n\u002F\u002F trade-off: a version based on max(updated_at) changes whenever ANY product\n\u002F\u002F changes, invalidating the whole catalogue. Version per partition (category)\n\u002F\u002F if updates are frequent and localised.\n","javascript","",[33,380,381,389,410,436,472,544,550,556,562],{"__ignoreMap":378},[382,383,385],"span",{"class":183,"line":384},1,[382,386,388],{"class":387},"sjfSM","\u002F\u002F On publish: version = short hash of the dataset's last update and schema version.\n",[382,390,392,396,400,403,407],{"class":183,"line":391},2,[382,393,395],{"class":394},"sPARh","import",[382,397,399],{"class":398},"saISM"," { createHash } ",[382,401,402],{"class":394},"from",[382,404,406],{"class":405},"sZ8jY"," 'node:crypto'",[382,408,409],{"class":398},";\n",[382,411,413,416,419,422,426,429,433],{"class":183,"line":412},3,[382,414,415],{"class":394},"export",[382,417,418],{"class":394}," async",[382,420,421],{"class":394}," function",[382,423,425],{"class":424},"smZ65"," catalogVersion",[382,427,428],{"class":398},"(",[382,430,432],{"class":431},"sQw3B","db",[382,434,435],{"class":398},") {\n",[382,437,439,442,445,449,452,455,458,461,464,466,469],{"class":183,"line":438},4,[382,440,441],{"class":394},"  const",[382,443,444],{"class":398}," { ",[382,446,448],{"class":447},"sPXB4","max",[382,450,451],{"class":398}," } ",[382,453,454],{"class":394},"=",[382,456,457],{"class":394}," await",[382,459,460],{"class":398}," db.",[382,462,463],{"class":424},"one",[382,465,428],{"class":398},[382,467,468],{"class":405},"'SELECT max(updated_at) AS max FROM products'",[382,470,471],{"class":398},");\n",[382,473,475,478,481,483,486,489,492,494,497,500,503,505,508,511,514,517,519,522,524,527,529,532,534,536,539,542],{"class":183,"line":474},5,[382,476,477],{"class":394},"  return",[382,479,480],{"class":424}," createHash",[382,482,428],{"class":398},[382,484,485],{"class":405},"'sha256'",[382,487,488],{"class":398},").",[382,490,491],{"class":424},"update",[382,493,428],{"class":398},[382,495,496],{"class":405},"`${",[382,498,499],{"class":447},"SCHEMA_VERSION",[382,501,502],{"class":405},"}:${",[382,504,448],{"class":398},[382,506,507],{"class":405},".",[382,509,510],{"class":424},"toISOString",[382,512,513],{"class":405},"()",[382,515,516],{"class":405},"}`",[382,518,488],{"class":398},[382,520,521],{"class":424},"digest",[382,523,428],{"class":398},[382,525,526],{"class":405},"'hex'",[382,528,488],{"class":398},[382,530,531],{"class":424},"slice",[382,533,428],{"class":398},[382,535,65],{"class":447},[382,537,538],{"class":398},", ",[382,540,541],{"class":447},"8",[382,543,471],{"class":398},[382,545,547],{"class":183,"line":546},6,[382,548,549],{"class":398},"}\n",[382,551,553],{"class":183,"line":552},7,[382,554,555],{"class":387},"\u002F\u002F trade-off: a version based on max(updated_at) changes whenever ANY product\n",[382,557,559],{"class":183,"line":558},8,[382,560,561],{"class":387},"\u002F\u002F changes, invalidating the whole catalogue. Version per partition (category)\n",[382,563,565],{"class":183,"line":564},9,[382,566,567],{"class":387},"\u002F\u002F if updates are frequent and localised.\n",[368,569,571],{"id":570},"_2-serve-versioned-urls-as-immutable","2. Serve versioned URLs as immutable",[373,573,575],{"className":375,"code":574,"language":377,"meta":378,"style":378},"app.get('\u002Fapi\u002Fcatalog', async (req, res) => {\n  const current = await catalogVersion(db);\n  if (req.query.v !== current) return res.redirect(302, `\u002Fapi\u002Fcatalog?v=${current}`);   \u002F\u002F or 404\n  res.set('Cache-Control', 'public, max-age=31536000, immutable');\n  res.json(await loadCatalog());\n});\n\u002F\u002F trade-off: redirecting unknown versions to the current one keeps old clients\n\u002F\u002F working but adds a round trip; returning 404 forces clients to fetch the\n\u002F\u002F version first. Pick based on how clients learn the version.\n",[33,576,577,615,632,676,696,714,719,724,729],{"__ignoreMap":378},[382,578,579,582,585,587,590,592,595,598,601,603,606,609,612],{"class":183,"line":384},[382,580,581],{"class":398},"app.",[382,583,584],{"class":424},"get",[382,586,428],{"class":398},[382,588,589],{"class":405},"'\u002Fapi\u002Fcatalog'",[382,591,538],{"class":398},[382,593,594],{"class":394},"async",[382,596,597],{"class":398}," (",[382,599,600],{"class":431},"req",[382,602,538],{"class":398},[382,604,605],{"class":431},"res",[382,607,608],{"class":398},") ",[382,610,611],{"class":394},"=>",[382,613,614],{"class":398}," {\n",[382,616,617,619,622,625,627,629],{"class":183,"line":391},[382,618,441],{"class":394},[382,620,621],{"class":447}," current",[382,623,624],{"class":394}," =",[382,626,457],{"class":394},[382,628,425],{"class":424},[382,630,631],{"class":398},"(db);\n",[382,633,634,637,640,643,646,649,652,655,657,660,662,665,668,670,673],{"class":183,"line":412},[382,635,636],{"class":394},"  if",[382,638,639],{"class":398}," (req.query.v ",[382,641,642],{"class":394},"!==",[382,644,645],{"class":398}," current) ",[382,647,648],{"class":394},"return",[382,650,651],{"class":398}," res.",[382,653,654],{"class":424},"redirect",[382,656,428],{"class":398},[382,658,659],{"class":447},"302",[382,661,538],{"class":398},[382,663,664],{"class":405},"`\u002Fapi\u002Fcatalog?v=${",[382,666,667],{"class":398},"current",[382,669,516],{"class":405},[382,671,672],{"class":398},");   ",[382,674,675],{"class":387},"\u002F\u002F or 404\n",[382,677,678,681,684,686,689,691,694],{"class":183,"line":438},[382,679,680],{"class":398},"  res.",[382,682,683],{"class":424},"set",[382,685,428],{"class":398},[382,687,688],{"class":405},"'Cache-Control'",[382,690,538],{"class":398},[382,692,693],{"class":405},"'public, max-age=31536000, immutable'",[382,695,471],{"class":398},[382,697,698,700,703,705,708,711],{"class":183,"line":474},[382,699,680],{"class":398},[382,701,702],{"class":424},"json",[382,704,428],{"class":398},[382,706,707],{"class":394},"await",[382,709,710],{"class":424}," loadCatalog",[382,712,713],{"class":398},"());\n",[382,715,716],{"class":183,"line":546},[382,717,718],{"class":398},"});\n",[382,720,721],{"class":183,"line":552},[382,722,723],{"class":387},"\u002F\u002F trade-off: redirecting unknown versions to the current one keeps old clients\n",[382,725,726],{"class":183,"line":558},[382,727,728],{"class":387},"\u002F\u002F working but adds a round trip; returning 404 forces clients to fetch the\n",[382,730,731],{"class":183,"line":564},[382,732,733],{"class":387},"\u002F\u002F version first. Pick based on how clients learn the version.\n",[368,735,737],{"id":736},"_3-deliver-the-current-version-to-clients","3. Deliver the current version to clients",[15,739,740,741,744,745,748,749,751],{},"Embed it in the server-rendered HTML (",[33,742,743],{},"\u003Cmeta name=\"catalog-version\" content=\"8f3a\">","), in a small uncached manifest endpoint (",[33,746,747],{},"\u002Fapi\u002Fversions"," with ",[33,750,227],{},"), or in a response header on another request the client already makes.",[368,753,755],{"id":754},"_4-keep-old-versions-answerable-for-a-while","4. Keep old versions answerable for a while",[15,757,758],{},"Clients holding an old version (open tabs, cached HTML) should either receive the old data (if you keep it) or be redirected. Never serve new data under an old version URL — that would poison immutable caches.",[15,760,761],{},[52,762,59,765,59,768,59,771,59,773,59,776,59,778,59,783,59,788,59,792,59,796,59,799,59,803,59,807,59,811,59,813,59,815,59,817,59,820,59,824,59,828,59,832,59,834,59,838,59,840,59,843,59,846,59,850,59,852,59,854,59,856,59,859,59,862,59,866,59,868,59,870,59,872,59],{"viewBox":763,"width":55,"role":56,"ariaLabel":764,"style":58},"0 0 760 244","Options for telling clients the current data version, with their cost and freshness.",[61,766],{"className":767,"x":65,"y":65,"width":55,"height":55,"fill":66},[64],[68,769,770],{},"Ways to distribute the current version",[72,772,764],{},[61,774],{"x":94,"y":94,"width":95,"height":775,"rx":97,"fill":98,"stroke":90,"style":99},"242",[101,777,770],{"x":103,"y":104,"fill":90,"style":105},[61,779],{"x":103,"y":108,"width":780,"height":781,"rx":65,"fill":90,"stroke":90,"style":782},"210.0","30.0","fill-opacity:0.06;stroke-opacity:0.4",[101,784,787],{"x":785,"y":786,"fill":90,"style":310,"textAnchor":315},"38.0","75.5","Method",[61,789],{"x":790,"y":108,"width":791,"height":781,"rx":65,"fill":90,"stroke":90,"style":782},"238.0","247.0",[101,793,795],{"x":794,"y":786,"fill":90,"style":310,"textAnchor":119},"361.5","Extra request?",[61,797],{"x":798,"y":108,"width":791,"height":781,"rx":65,"fill":90,"stroke":90,"style":782},"485.0",[101,800,802],{"x":801,"y":786,"fill":90,"style":310,"textAnchor":119},"608.5","Freshness",[61,804],{"x":103,"y":805,"width":780,"height":781,"rx":65,"fill":98,"stroke":90,"style":806},"86.0","stroke-opacity:0.35",[101,808,810],{"x":785,"y":809,"fill":90,"style":310,"textAnchor":315},"105.5","Meta tag in server HTML",[61,812],{"x":790,"y":805,"width":791,"height":781,"rx":65,"fill":112,"stroke":112,"style":113},[101,814,98],{"x":794,"y":809,"fill":90,"style":124,"textAnchor":119},[61,816],{"x":798,"y":805,"width":791,"height":781,"rx":65,"fill":90,"stroke":90,"style":782},[101,818,819],{"x":801,"y":809,"fill":90,"style":124,"textAnchor":119},"as fresh as the HTML",[61,821],{"x":103,"y":822,"width":780,"height":823,"rx":65,"fill":98,"stroke":90,"style":806},"116.0","46.0",[101,825,827],{"x":785,"y":826,"fill":90,"style":310,"textAnchor":315},"135.5","Small manifest endpoint",[101,829,831],{"x":785,"y":830,"fill":90,"style":310,"textAnchor":315},"151.5","(no-cache)",[61,833],{"x":790,"y":822,"width":791,"height":823,"rx":65,"fill":90,"stroke":90,"style":782},[101,835,837],{"x":794,"y":836,"fill":90,"style":124,"textAnchor":119},"143.5","one cheap request",[61,839],{"x":798,"y":822,"width":791,"height":823,"rx":65,"fill":112,"stroke":112,"style":113},[101,841,842],{"x":801,"y":836,"fill":90,"style":124,"textAnchor":119},"always current",[61,844],{"x":103,"y":845,"width":780,"height":781,"rx":65,"fill":98,"stroke":90,"style":806},"162.0",[101,847,849],{"x":785,"y":848,"fill":90,"style":310,"textAnchor":315},"181.5","Header on another API call",[61,851],{"x":790,"y":845,"width":791,"height":781,"rx":65,"fill":112,"stroke":112,"style":113},[101,853,98],{"x":794,"y":848,"fill":90,"style":124,"textAnchor":119},[61,855],{"x":798,"y":845,"width":791,"height":781,"rx":65,"fill":112,"stroke":112,"style":113},[101,857,858],{"x":801,"y":848,"fill":90,"style":124,"textAnchor":119},"current per call",[61,860],{"x":103,"y":861,"width":780,"height":781,"rx":65,"fill":98,"stroke":90,"style":806},"192.0",[101,863,865],{"x":785,"y":864,"fill":90,"style":310,"textAnchor":315},"211.5","Build-time constant",[61,867],{"x":790,"y":861,"width":791,"height":781,"rx":65,"fill":112,"stroke":112,"style":113},[101,869,98],{"x":794,"y":864,"fill":90,"style":124,"textAnchor":119},[61,871],{"x":798,"y":861,"width":791,"height":781,"rx":65,"fill":139,"stroke":140,"style":141},[101,873,874],{"x":801,"y":864,"fill":90,"style":124,"textAnchor":119},"only changes on deploy",[204,876,878],{"id":877},"verification","Verification",[15,880,881],{},"Request a versioned URL twice: the second request should be served from the browser cache without a network request. Publish a data change and confirm the version in HTML or the manifest changes and clients request the new URL. Confirm old version URLs behave as designed (old data or redirect). Monitor API request volume — it should fall sharply for versioned datasets.",[204,883,885],{"id":884},"worked-example-navigation-and-translations","Worked Example: Navigation and Translations",[15,887,888,889,892],{},"A multilingual storefront fetched its navigation tree and translation strings on every page view with ",[33,890,891],{},"max-age=60",". Both changed a few times a week. Versioning them by publish ID, embedding the versions in the HTML, and serving versioned URLs as immutable eliminated those requests on all but the first page view after each publish. API traffic fell by 38%, and the client-rendered header painted 120ms sooner on repeat views because both resources came from the browser cache.",[204,894,896],{"id":895},"combining-versioning-with-purging","Combining Versioning With Purging",[15,898,899],{},"Versioning and purging are complementary. Use versioning for datasets that change in discrete publishes and are fetched by your own clients — where you control how the version is distributed. Use purging for resources fetched at stable URLs you cannot change (HTML, public endpoints consumed by third parties, SEO-relevant pages). Many systems version their data APIs and purge their HTML, so a publish purges the HTML, the fresh HTML carries the new data version, and clients fetch the new versioned data — immutable caching everywhere except the one document that must stay addressable.",[204,901,903],{"id":902},"rolling-out-versioned-urls","Rolling Out Versioned URLs",[15,905,906,907,909],{},"Introduce versioning alongside the existing endpoint rather than replacing it: keep ",[33,908,35],{}," working with its old short TTL, add the versioned form, and migrate clients one at a time. Log requests to the unversioned URL so you know when the last client has moved. During the transition, make sure both forms are generated from the same data at the same moment — a versioned URL must never be filled with data from before its version was published, or immutable caches will hold the wrong content for a year. A simple guard is to compute the version and read the data inside the same transaction or snapshot.",[204,911,913],{"id":912},"common-mistakes","Common Mistakes",[209,915,916,922,928,934],{},[212,917,918,921],{},[215,919,920],{},"Reusing a version for different data."," Immutable caches will serve the old content forever; versions must be unique per content state.",[212,923,924,927],{},[215,925,926],{},"Versioning per request instead of per dataset."," Timestamps or random values in versions destroy caching.",[212,929,930,933],{},[215,931,932],{},"Forgetting schema changes."," Include a schema or API version so a deploy that changes response shape changes the URL.",[212,935,936,939],{},[215,937,938],{},"Long-caching the version manifest."," The manifest must be fresh; cache it briefly or revalidate.",[204,941,943],{"id":942},"edge-cases","Edge Cases",[15,945,946,949],{},[215,947,948],{},"Partial updates."," Large datasets updated frequently in small parts benefit from per-partition versions (per category, per locale).",[15,951,952,955],{},[215,953,954],{},"User-specific data."," Versioning applies to shared data. Personal data should remain private and short-lived.",[15,957,958,961],{},[215,959,960],{},"Service workers."," Precaching versioned data URLs works naturally; old versions can be pruned on activation.",[15,963,964,967],{},[215,965,966],{},"GraphQL."," Persisted queries plus a data version variable give cacheable, versioned GET requests.",[204,969,971],{"id":970},"faq","FAQ",[973,974,977,981],"details",{"className":975},[976],"faq-item",[978,979,980],"summary",{},"Is a query parameter or a path segment better for the version?",[15,982,983],{},"Both work with modern CDNs as long as the version is part of the cache key. Path segments avoid any CDN rule that strips or ignores query parameters; query parameters are easier to add to existing endpoints.",[973,985,987,990],{"className":986},[976],[978,988,989],{},"What happens to old versions in the cache?",[15,991,992],{},"They remain until evicted by inactivity or storage pressure. Since nothing references them, they are harmless and eventually disappear.",[973,994,996,999],{"className":995},[976],[978,997,998],{},"Can versioning replace ETags?",[15,1000,1001],{},"For versioned URLs, revalidation never happens during the TTL, so ETags matter little. Keep them on unversioned endpoints such as the version manifest.",[973,1003,1005,1008],{"className":1004},[976],[978,1006,1007],{},"How fast do clients pick up a new version?",[15,1009,1010],{},"As fast as they learn it: immediately on the next page load if it is embedded in fresh HTML, or on the next manifest check otherwise. Long-lived SPA sessions should check the manifest periodically or on focus.",[973,1012,1014,1017],{"className":1013},[976],[978,1015,1016],{},"Does this work for search results?",[15,1018,1019],{},"Only partially. Search responses depend on queries and often on rapidly changing inventory. Version the underlying index (so a reindex changes URLs) and keep TTLs moderate.",[973,1021,1023,1026],{"className":1022},[976],[978,1024,1025],{},"Is a content hash better than a counter?",[15,1027,1028],{},"A hash of the data is self-verifying and stable across environments; a counter is simpler and smaller. Either works if every content change produces a new value.",[204,1030,1032],{"id":1031},"related","Related",[209,1034,1035,1042,1049],{},[212,1036,1037,1041],{},[19,1038,1040],{"href":1039},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Finvalidating-immutable-hashed-assets-safely\u002F","Invalidating immutable hashed assets safely"," — the same principle for static files.",[212,1043,1044,1048],{},[19,1045,1047],{"href":1046},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fcaching-api-responses-at-the-cdn\u002F","Caching API responses at the CDN"," — the purge-based alternative.",[212,1050,1051,1055],{},[19,1052,1054],{"href":1053},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fsoft-purge-vs-hard-purge\u002F","Soft purge vs hard purge"," — when purging is still the right tool.",[1057,1058,1060],"script",{"type":1059},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Version API Responses for Cache Busting\",\n  \"description\": \"How content-versioned API URLs make data cacheable like hashed assets, and how to distribute the current version to clients.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Compute a version for the dataset\",\n      \"text\": \"Compute a version for the dataset\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Serve versioned URLs as immutable\",\n      \"text\": \"Serve versioned URLs as immutable\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Deliver the current version to clients\",\n      \"text\": \"Embed it in the server-rendered HTML (), in a small uncached manifest endpoint (\u002Fapi\u002Fversions with no-cache), or in a response header on another request the client already makes.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Keep old versions answerable for a while\",\n      \"text\": \"Clients holding an old version (open tabs, cached HTML) should either receive the old data (if you keep it) or be redirected.\"\n    }\n  ]\n}\n",[1057,1062,1063],{"type":1059},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Version API Responses for Cache Busting\",\n  \"description\": \"How content-versioned API URLs make data cacheable like hashed assets, and how to distribute the current version to clients.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fversioning-api-responses-for-cache-busting\u002F\"\n  }\n}\n",[1057,1065,1066],{"type":1059},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Advanced Caching Strategies & CDN Architecture\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Cache Invalidation Patterns\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Versioning API Responses for Cache Busting\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fversioning-api-responses-for-cache-busting\u002F\"\n    }\n  ]\n}\n",[1068,1069,1070],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}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);}",{"title":378,"searchDepth":391,"depth":391,"links":1072},[1073,1074,1075,1081,1082,1083,1084,1085,1086,1087,1088],{"id":206,"depth":391,"text":207},{"id":247,"depth":391,"text":248},{"id":365,"depth":391,"text":366,"children":1076},[1077,1078,1079,1080],{"id":370,"depth":412,"text":371},{"id":570,"depth":412,"text":571},{"id":736,"depth":412,"text":737},{"id":754,"depth":412,"text":755},{"id":877,"depth":391,"text":878},{"id":884,"depth":391,"text":885},{"id":895,"depth":391,"text":896},{"id":902,"depth":391,"text":903},{"id":912,"depth":391,"text":913},{"id":942,"depth":391,"text":943},{"id":970,"depth":391,"text":971},{"id":1031,"depth":391,"text":1032},"How content-versioned API URLs make data cacheable like hashed assets, and how to distribute the current version to clients.","md",{"slug":1092,"type":1093,"breadcrumb":1094,"datePublished":1102,"dateModified":1102},"versioning-api-responses-for-cache-busting","article",[1095,1098,1099,1100],{"name":1096,"url":1097},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1101},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fversioning-api-responses-for-cache-busting\u002F","2026-10-06",true,"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fversioning-api-responses-for-cache-busting",{"title":1106,"description":1107},"Versioning API Responses Instead of Purging Caches","Put a data version in API URLs so responses can be cached for a long time and invalidated by changing the reference — no purges, no stale data, high hit rates.","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fversioning-api-responses-for-cache-busting\u002Findex","QZOdXyvzunC9ARsS2UHcmi2Kahm29g7SyRccXldPmws",[1111,1115],{"title":1112,"path":1113,"stem":1114,"children":-1},"Soft Purge vs Hard Purge","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fsoft-purge-vs-hard-purge","advanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fsoft-purge-vs-hard-purge\u002Findex",{"title":1116,"path":1117,"stem":1118,"children":-1},"CDN Edge Caching Configuration","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration","advanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Findex",1791308075053]