[{"data":1,"prerenderedAt":1063},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fcaching-api-responses-at-the-cdn":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fcaching-api-responses-at-the-cdn":1055},{"id":4,"title":5,"body":6,"description":1035,"extension":1036,"meta":1037,"navigation":1048,"path":1049,"seo":1050,"stem":1053,"__hash__":1054},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fcaching-api-responses-at-the-cdn\u002Findex.md","Caching API Responses at the CDN",{"type":7,"value":8,"toc":1019},"minimark",[9,14,34,37,196,201,249,253,263,273,279,285,348,352,357,546,549,553,559,563,570,673,677,684,803,807,810,814,825,829,866,870,876,882,888,894,898,910,923,932,941,950,959,968,977,981,1004,1009,1012,1015],[10,11,13],"h1",{"id":12},"how-to-cache-api-responses-at-the-cdn","How to Cache API Responses at the CDN",[15,16,17,18,23,24,28,29,33],"p",{},"This guide extends ",[19,20,22],"a",{"href":21},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002F","CDN Edge Caching Configuration"," to JSON APIs, within ",[19,25,27],{"href":26},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",". Modern frontends spend much of their loading time waiting for data: product details, search results, navigation menus, CMS content, configuration. Those requests usually go straight to the origin with ",[30,31,32],"code",{},"Cache-Control: no-cache"," or no caching headers at all, even when the response is the same for every user and changes only occasionally.",[15,35,36],{},"Caching public GET responses at the CDN cuts their latency from origin round-trip time (often 150–500ms) to edge latency (20–60ms), which shortens client-rendered LCP, speeds up route transitions in single-page apps, and protects the origin during spikes. The challenges are deciding what is safe to cache, choosing TTLs, keeping keys tidy, and purging when underlying data changes.",[15,38,39],{},[40,41,47,48,47,55,47,59,47,62,47,71,47,77,47,83,47,90,47,94,47,99,47,102,47,106,47,110,47,114,47,118,47,122,47,124,47,127,47,130,47,134,47,136,47,139,47,141,47,144,47,147,47,151,47,153,47,155,47,157,47,160,47,163,47,167,47,172,47,175,47,177,47,180,47,183,47,187,47,189,47,191,47,193,47],"svg",{"viewBox":42,"width":43,"role":44,"ariaLabel":45,"style":46},"0 0 760 258","100%","img","Classification of common API endpoints by whether they can be cached at the CDN and the recommended header.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[49,50],"rect",{"className":51,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],"svg-canvas","0","#ffffff",[56,57,58],"title",{},"Which API responses can be cached at the edge",[60,61,45],"desc",{},[49,63],{"x":64,"y":64,"width":65,"height":66,"rx":67,"fill":68,"stroke":69,"style":70},"1","758","256","10","none","currentColor","stroke-opacity:0.18",[72,73,58],"text",{"x":74,"y":75,"fill":69,"style":76},"28.0","34.0","font-size:16px;font-weight:700",[49,78],{"x":74,"y":79,"width":80,"height":81,"rx":53,"fill":69,"stroke":69,"style":82},"56.0","196.8","30.0","fill-opacity:0.06;stroke-opacity:0.4",[72,84,89],{"x":85,"y":86,"fill":69,"style":87,"textAnchor":88},"38.0","75.5","font-size:12.5px;font-weight:700","start","Endpoint",[49,91],{"x":92,"y":79,"width":93,"height":81,"rx":53,"fill":69,"stroke":69,"style":82},"224.8","253.6",[72,95,98],{"x":96,"y":86,"fill":69,"style":87,"textAnchor":97},"351.6","middle","Same for all users?",[49,100],{"x":101,"y":79,"width":93,"height":81,"rx":53,"fill":69,"stroke":69,"style":82},"478.4",[72,103,105],{"x":104,"y":86,"fill":69,"style":87,"textAnchor":97},"605.2","Recommended policy",[49,107],{"x":74,"y":108,"width":80,"height":81,"rx":53,"fill":68,"stroke":69,"style":109},"86.0","stroke-opacity:0.35",[72,111,113],{"x":85,"y":112,"fill":69,"style":87,"textAnchor":88},"105.5","Product \u002F article detail",[49,115],{"x":92,"y":108,"width":93,"height":81,"rx":53,"fill":116,"stroke":116,"style":117},"#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[72,119,121],{"x":96,"y":112,"fill":69,"style":120,"textAnchor":97},"font-size:12px","yes",[49,123],{"x":101,"y":108,"width":93,"height":81,"rx":53,"fill":68,"stroke":69,"style":109},[72,125,126],{"x":104,"y":112,"fill":69,"style":120,"textAnchor":97},"s-maxage + SWR, purge by tag",[49,128],{"x":74,"y":129,"width":80,"height":81,"rx":53,"fill":68,"stroke":69,"style":109},"116.0",[72,131,133],{"x":85,"y":132,"fill":69,"style":87,"textAnchor":88},"135.5","Search results",[49,135],{"x":92,"y":129,"width":93,"height":81,"rx":53,"fill":116,"stroke":116,"style":117},[72,137,138],{"x":96,"y":132,"fill":69,"style":120,"textAnchor":97},"yes (per query)",[49,140],{"x":101,"y":129,"width":93,"height":81,"rx":53,"fill":68,"stroke":69,"style":109},[72,142,143],{"x":104,"y":132,"fill":69,"style":120,"textAnchor":97},"short s-maxage, normalised key",[49,145],{"x":74,"y":146,"width":80,"height":81,"rx":53,"fill":68,"stroke":69,"style":109},"146.0",[72,148,150],{"x":85,"y":149,"fill":69,"style":87,"textAnchor":88},"165.5","Navigation \u002F config",[49,152],{"x":92,"y":146,"width":93,"height":81,"rx":53,"fill":116,"stroke":116,"style":117},[72,154,121],{"x":96,"y":149,"fill":69,"style":120,"textAnchor":97},[49,156],{"x":101,"y":146,"width":93,"height":81,"rx":53,"fill":68,"stroke":69,"style":109},[72,158,159],{"x":104,"y":149,"fill":69,"style":120,"textAnchor":97},"long s-maxage, purge on publish",[49,161],{"x":74,"y":162,"width":80,"height":81,"rx":53,"fill":68,"stroke":69,"style":109},"176.0",[72,164,166],{"x":85,"y":165,"fill":69,"style":87,"textAnchor":88},"195.5","Cart \u002F account",[49,168],{"x":92,"y":162,"width":93,"height":81,"rx":53,"fill":169,"stroke":170,"style":171},"#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[72,173,174],{"x":96,"y":165,"fill":69,"style":120,"textAnchor":97},"no",[49,176],{"x":101,"y":162,"width":93,"height":81,"rx":53,"fill":68,"stroke":69,"style":109},[72,178,179],{"x":104,"y":165,"fill":69,"style":120,"textAnchor":97},"private, no-store",[49,181],{"x":74,"y":182,"width":80,"height":81,"rx":53,"fill":68,"stroke":69,"style":109},"206.0",[72,184,186],{"x":85,"y":185,"fill":69,"style":87,"textAnchor":88},"225.5","Prices with personal discounts",[49,188],{"x":92,"y":182,"width":93,"height":81,"rx":53,"fill":169,"stroke":170,"style":171},[72,190,174],{"x":96,"y":185,"fill":69,"style":120,"textAnchor":97},[49,192],{"x":101,"y":182,"width":93,"height":81,"rx":53,"fill":68,"stroke":69,"style":109},[72,194,195],{"x":104,"y":185,"fill":69,"style":120,"textAnchor":97},"private; or split base price out",[197,198,200],"h2",{"id":199},"rapid-diagnosis","Rapid Diagnosis",[202,203,204,212,237,243],"ul",{},[205,206,207,211],"li",{},[208,209,210],"strong",{},"List API calls on key routes"," in the Network panel with their timings and cache headers.",[205,213,214,225,226,229,230,229,233,236],{},[208,215,216,217,220,221,224],{},"Check ",[30,218,219],{},"cf-cache-status",", ",[30,222,223],{},"x-cache"," or equivalent"," on API responses; ",[30,227,228],{},"MISS","\u002F",[30,231,232],{},"DYNAMIC",[30,234,235],{},"BYPASS"," on every request means no edge caching.",[205,238,239,242],{},[208,240,241],{},"Classify each endpoint"," as public or personal, and note how often its data changes.",[205,244,245,248],{},[208,246,247],{},"Check request methods."," GraphQL APIs often use POST for queries, which CDNs do not cache by default.",[197,250,252],{"id":251},"root-cause-analysis","Root Cause Analysis",[15,254,255,258,259,262],{},[208,256,257],{},"1. Conservative default headers."," API frameworks often send ",[30,260,261],{},"no-cache"," or nothing, which many CDNs treat as uncacheable for JSON.",[15,264,265,268,269,272],{},[208,266,267],{},"2. Authentication on public data."," Public endpoints that accept (and ignore) auth headers or cookies get treated as personal by CDNs that bypass on ",[30,270,271],{},"Authorization",".",[15,274,275,278],{},[208,276,277],{},"3. POST-based queries."," GraphQL over POST and RPC-style endpoints cannot be cached without mapping them to GET.",[15,280,281,284],{},[208,282,283],{},"4. No invalidation strategy."," Teams avoid caching because they cannot purge reliably when data changes.",[15,286,287],{},[40,288,47,291,47,294,47,297,47,299,47,302,47,304,47,311,47,318,47,323,47,327,47,332,47,336,47,340,47,344,47],{"viewBox":289,"width":43,"role":44,"ariaLabel":290,"style":46},"0 0 760 163","Bar chart of p75 latency for a product detail API served from origin and from the CDN cache.",[49,292],{"className":293,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,295,296],{},"Product API latency from the browser (p75)",[60,298,290],{},[49,300],{"x":64,"y":64,"width":65,"height":301,"rx":67,"fill":68,"stroke":69,"style":70},"161",[72,303,296],{"x":74,"y":75,"fill":69,"style":76},[72,305,310],{"x":306,"y":307,"fill":69,"style":308,"textAnchor":309},"211.7","70.0","font-size:13px","end","Origin (no cache)",[49,312],{"x":313,"y":79,"width":314,"height":315,"rx":316,"fill":169,"stroke":170,"style":317},"223.7","444.3","19","3","fill-opacity:0.7;stroke-opacity:0.9",[72,319,322],{"x":320,"y":307,"fill":69,"style":321},"674.0","font-size:12px;font-weight:600","310ms",[72,324,326],{"x":306,"y":325,"fill":69,"style":308,"textAnchor":309},"101.0","CDN hit",[49,328],{"x":313,"y":329,"width":330,"height":315,"rx":316,"fill":116,"stroke":116,"style":331},"87.0","54.5","fill-opacity:0.55;stroke-opacity:0.9",[72,333,335],{"x":334,"y":325,"fill":69,"style":321},"284.1","38ms",[72,337,339],{"x":306,"y":338,"fill":69,"style":308,"textAnchor":309},"132.0","CDN stale + background refresh",[49,341],{"x":313,"y":342,"width":343,"height":315,"rx":316,"fill":116,"stroke":116,"style":331},"118.0","58.8",[72,345,347],{"x":346,"y":338,"fill":69,"style":321},"288.4","41ms",[197,349,351],{"id":350},"step-by-step-resolution","Step-by-Step Resolution",[353,354,356],"h3",{"id":355},"_1-send-explicit-shared-cache-headers-on-public-endpoints","1. Send explicit shared-cache headers on public endpoints",[358,359,364],"pre",{"className":360,"code":361,"language":362,"meta":363,"style":363},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Express: public product endpoint.\napp.get('\u002Fapi\u002Fproducts\u002F:id', async (req, res) => {\n  const product = await getProduct(req.params.id);\n  res.set('Cache-Control', 'public, max-age=0, s-maxage=300, stale-while-revalidate=3600');\n  res.set('Cache-Tag', `product-${product.id}`);              \u002F\u002F surrogate key for purging\n  res.set('Vary', 'Accept-Encoding');\n  res.json(product);\n});\n\u002F\u002F trade-off: max-age=0 keeps browsers revalidating so they never hold stale\n\u002F\u002F data longer than the CDN; raise it only for data that tolerates staleness.\n","javascript","",[30,365,366,375,420,442,464,498,517,528,534,540],{"__ignoreMap":363},[367,368,371],"span",{"class":369,"line":370},"line",1,[367,372,374],{"class":373},"sjfSM","\u002F\u002F Express: public product endpoint.\n",[367,376,378,382,386,389,393,395,399,402,406,408,411,414,417],{"class":369,"line":377},2,[367,379,381],{"class":380},"saISM","app.",[367,383,385],{"class":384},"smZ65","get",[367,387,388],{"class":380},"(",[367,390,392],{"class":391},"sZ8jY","'\u002Fapi\u002Fproducts\u002F:id'",[367,394,220],{"class":380},[367,396,398],{"class":397},"sPARh","async",[367,400,401],{"class":380}," (",[367,403,405],{"class":404},"sQw3B","req",[367,407,220],{"class":380},[367,409,410],{"class":404},"res",[367,412,413],{"class":380},") ",[367,415,416],{"class":397},"=>",[367,418,419],{"class":380}," {\n",[367,421,423,426,430,433,436,439],{"class":369,"line":422},3,[367,424,425],{"class":397},"  const",[367,427,429],{"class":428},"sPXB4"," product",[367,431,432],{"class":397}," =",[367,434,435],{"class":397}," await",[367,437,438],{"class":384}," getProduct",[367,440,441],{"class":380},"(req.params.id);\n",[367,443,445,448,451,453,456,458,461],{"class":369,"line":444},4,[367,446,447],{"class":380},"  res.",[367,449,450],{"class":384},"set",[367,452,388],{"class":380},[367,454,455],{"class":391},"'Cache-Control'",[367,457,220],{"class":380},[367,459,460],{"class":391},"'public, max-age=0, s-maxage=300, stale-while-revalidate=3600'",[367,462,463],{"class":380},");\n",[367,465,467,469,471,473,476,478,481,484,486,489,492,495],{"class":369,"line":466},5,[367,468,447],{"class":380},[367,470,450],{"class":384},[367,472,388],{"class":380},[367,474,475],{"class":391},"'Cache-Tag'",[367,477,220],{"class":380},[367,479,480],{"class":391},"`product-${",[367,482,483],{"class":380},"product",[367,485,272],{"class":391},[367,487,488],{"class":380},"id",[367,490,491],{"class":391},"}`",[367,493,494],{"class":380},");              ",[367,496,497],{"class":373},"\u002F\u002F surrogate key for purging\n",[367,499,501,503,505,507,510,512,515],{"class":369,"line":500},6,[367,502,447],{"class":380},[367,504,450],{"class":384},[367,506,388],{"class":380},[367,508,509],{"class":391},"'Vary'",[367,511,220],{"class":380},[367,513,514],{"class":391},"'Accept-Encoding'",[367,516,463],{"class":380},[367,518,520,522,525],{"class":369,"line":519},7,[367,521,447],{"class":380},[367,523,524],{"class":384},"json",[367,526,527],{"class":380},"(product);\n",[367,529,531],{"class":369,"line":530},8,[367,532,533],{"class":380},"});\n",[367,535,537],{"class":369,"line":536},9,[367,538,539],{"class":373},"\u002F\u002F trade-off: max-age=0 keeps browsers revalidating so they never hold stale\n",[367,541,543],{"class":369,"line":542},10,[367,544,545],{"class":373},"\u002F\u002F data longer than the CDN; raise it only for data that tolerates staleness.\n",[15,547,548],{},"Expected outcome: the CDN caches the response for five minutes and serves stale copies while refreshing.",[353,550,552],{"id":551},"_2-ignore-auth-headers-on-public-routes-at-the-edge","2. Ignore auth headers on public routes at the edge",[15,554,555,556,558],{},"Strip ",[30,557,271],{}," and cookies from requests to public endpoints before they reach the cache (and the origin), so the CDN does not bypass caching and the origin cannot accidentally personalise.",[353,560,562],{"id":561},"_3-map-cacheable-post-queries-to-get","3. Map cacheable POST queries to GET",[15,564,565,566,569],{},"For GraphQL, use persisted queries sent as GET (",[30,567,568],{},"\u002Fgraphql?id=\u003Chash>&variables=...","), which CDNs can cache by URL.",[358,571,573],{"className":360,"code":572,"language":362,"meta":363,"style":363},"\u002F\u002F Client: persisted query as GET.\nconst url = `\u002Fgraphql?id=${QUERY_HASH}&variables=${encodeURIComponent(JSON.stringify(vars))}`;\nconst data = await fetch(url).then((r) => r.json());\n\u002F\u002F trade-off: persisted queries need a registry of allowed query hashes on the\n\u002F\u002F server. That is also a security benefit, but it adds a build step.\n",[30,574,575,580,625,663,668],{"__ignoreMap":363},[367,576,577],{"class":369,"line":370},[367,578,579],{"class":373},"\u002F\u002F Client: persisted query as GET.\n",[367,581,582,585,588,590,593,596,599,602,604,607,609,612,614,617,620,622],{"class":369,"line":377},[367,583,584],{"class":397},"const",[367,586,587],{"class":428}," url",[367,589,432],{"class":397},[367,591,592],{"class":391}," `\u002Fgraphql?id=${",[367,594,595],{"class":428},"QUERY_HASH",[367,597,598],{"class":391},"}&variables=${",[367,600,601],{"class":384},"encodeURIComponent",[367,603,388],{"class":391},[367,605,606],{"class":428},"JSON",[367,608,272],{"class":391},[367,610,611],{"class":384},"stringify",[367,613,388],{"class":391},[367,615,616],{"class":380},"vars",[367,618,619],{"class":391},"))",[367,621,491],{"class":391},[367,623,624],{"class":380},";\n",[367,626,627,629,632,634,636,639,642,645,648,651,653,655,658,660],{"class":369,"line":422},[367,628,584],{"class":397},[367,630,631],{"class":428}," data",[367,633,432],{"class":397},[367,635,435],{"class":397},[367,637,638],{"class":384}," fetch",[367,640,641],{"class":380},"(url).",[367,643,644],{"class":384},"then",[367,646,647],{"class":380},"((",[367,649,650],{"class":404},"r",[367,652,413],{"class":380},[367,654,416],{"class":397},[367,656,657],{"class":380}," r.",[367,659,524],{"class":384},[367,661,662],{"class":380},"());\n",[367,664,665],{"class":369,"line":444},[367,666,667],{"class":373},"\u002F\u002F trade-off: persisted queries need a registry of allowed query hashes on the\n",[367,669,670],{"class":369,"line":466},[367,671,672],{"class":373},"\u002F\u002F server. That is also a security benefit, but it adds a build step.\n",[353,674,676],{"id":675},"_4-purge-by-tag-when-data-changes","4. Purge by tag when data changes",[15,678,679,680,683],{},"When a product changes, purge ",[30,681,682],{},"product-\u003Cid>"," so every cached response that includes it (detail, listings, search) is refreshed.",[15,685,686],{},[40,687,47,690,47,693,47,696,47,698,47,715,47,718,47,720,47,725,47,730,47,733,47,737,47,740,47,744,47,747,47,751,47,758,47,761,47,764,47,767,47,773,47,778,47,782,47,787,47,790,47,795,47,799,47],{"viewBox":688,"width":43,"role":44,"ariaLabel":689,"style":46},"0 0 760 290","Sequence showing an admin update triggering a tag purge, followed by the next request refetching fresh data.",[49,691],{"className":692,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,694,695],{},"Purge-on-write for a cached API",[60,697,689],{},[699,700,701],"defs",{},[702,703,710],"marker",{"id":704,"viewBox":705,"refX":706,"refY":707,"markerWidth":708,"markerHeight":708,"orient":709},"fadff4d734","0 0 10 10","9","5","7","auto-start-reverse",[711,712],"path",{"d":713,"fill":69,"style":714},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[49,716],{"x":64,"y":64,"width":65,"height":717,"rx":67,"fill":68,"stroke":69,"style":70},"288",[72,719,695],{"x":74,"y":75,"fill":69,"style":76},[49,721],{"x":722,"y":79,"width":723,"height":75,"rx":724,"fill":69,"stroke":69,"style":82},"41.0","150.0","6",[72,726,729],{"x":129,"y":727,"fill":69,"style":728,"textAnchor":97},"78.0","font-size:13px;font-weight:700","Admin",[49,731],{"x":732,"y":79,"width":723,"height":75,"rx":724,"fill":69,"stroke":69,"style":82},"217.0",[72,734,736],{"x":735,"y":727,"fill":69,"style":728,"textAnchor":97},"292.0","Origin",[49,738],{"x":739,"y":79,"width":723,"height":75,"rx":724,"fill":69,"stroke":69,"style":82},"393.0",[72,741,743],{"x":742,"y":727,"fill":69,"style":728,"textAnchor":97},"468.0","CDN",[49,745],{"x":746,"y":79,"width":723,"height":75,"rx":724,"fill":69,"stroke":69,"style":82},"569.0",[72,748,750],{"x":749,"y":727,"fill":69,"style":728,"textAnchor":97},"644.0","Client",[369,752],{"x1":129,"y1":753,"x2":129,"y2":754,"stroke":69,"strokeWidth":755,"strokeDashArray":756,"style":109},"90.0","268.0","1.5",[757,757],"4",[369,759],{"x1":735,"y1":753,"x2":735,"y2":754,"stroke":69,"strokeWidth":755,"strokeDashArray":760,"style":109},[757,757],[369,762],{"x1":742,"y1":753,"x2":742,"y2":754,"stroke":69,"strokeWidth":755,"strokeDashArray":763,"style":109},[757,757],[369,765],{"x1":749,"y1":753,"x2":749,"y2":754,"stroke":69,"strokeWidth":755,"strokeDashArray":766,"style":109},[757,757],[369,768],{"x1":129,"y1":769,"x2":770,"y2":769,"stroke":69,"strokeWidth":755,"style":771,"markerEnd":772},"124.0","290.0","stroke-opacity:0.6","url(#fadff4d734)",[72,774,777],{"x":775,"y":776,"fill":69,"style":120,"textAnchor":97},"204.0","117.0","update product 42",[369,779],{"x1":735,"y1":780,"x2":781,"y2":780,"stroke":69,"strokeWidth":755,"style":771,"markerEnd":772},"164.0","466.0",[72,783,786],{"x":784,"y":785,"fill":69,"style":120,"textAnchor":97},"380.0","157.0","purge tag product-42",[369,788],{"x1":749,"y1":775,"x2":789,"y2":775,"stroke":69,"strokeWidth":755,"style":771,"markerEnd":772},"470.0",[72,791,794],{"x":792,"y":793,"fill":69,"style":120,"textAnchor":97},"556.0","197.0","GET \u002Fapi\u002Fproducts\u002F42",[369,796],{"x1":742,"y1":797,"x2":798,"y2":797,"stroke":69,"strokeWidth":755,"style":771,"markerEnd":772},"244.0","294.0",[72,800,802],{"x":784,"y":801,"fill":69,"style":120,"textAnchor":97},"237.0","miss → fetch fresh",[197,804,806],{"id":805},"verification","Verification",[15,808,809],{},"Request each public endpoint twice and confirm a cache HIT on the second request; request a personal endpoint and confirm it is never cached. Update a record and confirm the next request returns fresh data after the purge. In RUM, measure data request latency (Resource Timing for API URLs) and route transition times; both should fall.",[197,811,813],{"id":812},"worked-example-category-listing-api","Worked Example: Category Listing API",[15,815,816,817,820,821,824],{},"An e-commerce SPA fetched category listings from ",[30,818,819],{},"\u002Fapi\u002Fcategories\u002F:slug\u002Fproducts?page=n&sort=…"," on every navigation, with origin latency of 280ms at p75. The team added ",[30,822,823],{},"s-maxage=120, stale-while-revalidate=600",", normalised the query parameters in the cache key, tagged responses with every product ID and the category, and purged tags from the product-update pipeline. The edge hit ratio for listings reached 89%, data latency p75 fell to 45ms, and route transitions between categories became noticeably snappier; INP was unaffected, but route LCP improved by about 230ms.",[197,826,828],{"id":827},"common-mistakes","Common Mistakes",[202,830,831,837,847,853],{},[205,832,833,836],{},[208,834,835],{},"Caching responses that vary by user."," Personal discounts, inventory reserved for a user, or localisation from a profile must not be cached publicly.",[205,838,839,846],{},[208,840,841,842,845],{},"Long browser ",[30,843,844],{},"max-age"," on APIs."," Browsers cannot be purged; keep browser TTLs short or zero and let the CDN hold the data.",[205,848,849,852],{},[208,850,851],{},"Too many tags."," Some CDNs cap the size of tag headers; tag by entity IDs that actually drive purges.",[205,854,855,858,859,862,863,865],{},[208,856,857],{},"Forgetting CORS."," Cached responses must carry the right ",[30,860,861],{},"Access-Control-Allow-Origin","; vary on ",[30,864,736],{}," only if you serve different values.",[197,867,869],{"id":868},"edge-cases","Edge Cases",[15,871,872,875],{},[208,873,874],{},"Personalised fields inside public data."," Split them: cache the public product and fetch the user's price or wishlist status separately.",[15,877,878,881],{},[208,879,880],{},"Rate limits and quotas."," Cached responses reduce origin calls, but ensure rate limiting happens before the cache only for uncached routes, or legitimate cached traffic may be throttled.",[15,883,884,887],{},[208,885,886],{},"Large responses."," Very large JSON payloads cost transfer time even when cached; paginate and compress.",[15,889,890,893],{},[208,891,892],{},"Error responses."," Cache 404s briefly (seconds) and never cache 5xx, or an outage gets cached.",[197,895,897],{"id":896},"faq","FAQ",[899,900,903,907],"details",{"className":901},[902],"faq-item",[904,905,906],"summary",{},"Can I cache GraphQL at the CDN?",[15,908,909],{},"Yes, with GET-based persisted queries and responses that do not depend on the user. Client-side GraphQL caches (Apollo, urql) complement but do not replace edge caching, because they only help the same client.",[899,911,913,916],{"className":912},[902],[904,914,915],{},"How short can TTLs be and still help?",[15,917,918,919,922],{},"Even a few seconds helps under load — it collapses bursts of identical requests into one origin call. For user-facing latency, longer TTLs with ",[30,920,921],{},"stale-while-revalidate"," give the best results, with purges handling freshness.",[899,924,926,929],{"className":925},[902],[904,927,928],{},"Should API caching use the same rules as HTML?",[15,930,931],{},"The principles are the same — public vs private, keys, TTLs, purging — but APIs are usually more granular, so tag-based purging and parameter allow-lists matter more.",[899,933,935,938],{"className":934},[902],[904,936,937],{},"Does caching APIs help INP?",[15,939,940],{},"Indirectly: interactions that trigger data fetches (filters, pagination) complete their visible update sooner when data comes from the edge. INP measures the next paint after input, which may be a loading state, but users perceive the faster result.",[899,942,944,947],{"className":943},[902],[904,945,946],{},"What about server-side rendering that calls these APIs?",[15,948,949],{},"SSR servers benefit too if they call the API through the CDN (or a shared cache). That turns many page renders' data fetches into cache hits and lowers TTFB.",[899,951,953,956],{"className":952},[902],[904,954,955],{},"What about APIs called from other servers, not browsers?",[15,957,958],{},"Server-to-server calls (SSR fetching data, microservices) can use the same CDN or an internal shared cache. The headers and purge strategy are the same; the main difference is that you can often allow longer TTLs because the consumer is under your control.",[899,960,962,965],{"className":961},[902],[904,963,964],{},"How should I handle ETags on cached APIs?",[15,966,967],{},"Keep them. Strong or weak ETags let the CDN revalidate cheaply with the origin when a TTL expires, and let browsers revalidate with the CDN. Make sure ETags are stable across origin instances — ETags derived from server-specific values defeat revalidation.",[899,969,971,974],{"className":970},[902],[904,972,973],{},"Is it worth caching APIs with very low traffic?",[15,975,976],{},"Usually not for latency — each location rarely reuses the entry — but tiered caching changes that by concentrating requests at a shield. For low-traffic endpoints, focus on making the origin response itself fast and add caching mainly for spike protection.",[197,978,980],{"id":979},"related","Related",[202,982,983,990,997],{},[205,984,985,989],{},[19,986,988],{"href":987},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-data-fetching-with-swr-and-tanstack-query\u002F","Stale-while-revalidate data fetching with SWR and TanStack Query"," — the client-side cache on top.",[205,991,992,996],{},[19,993,995],{"href":994},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F","Purging CDN cache by tag on deploy"," — tag purging in depth.",[205,998,999,1003],{},[19,1000,1002],{"href":1001},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fversioning-api-responses-for-cache-busting\u002F","Versioning API responses for cache busting"," — an alternative to purging.",[1005,1006,1008],"script",{"type":1007},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Cache API Responses at the CDN\",\n  \"description\": \"Which API responses can be cached at the CDN, how to set headers and keys for them, and how to purge them when data changes.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Send explicit shared-cache headers on public endpoints\",\n      \"text\": \"Expected outcome: the CDN caches the response for five minutes and serves stale copies while refreshing.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Ignore auth headers on public routes at the edge\",\n      \"text\": \"Strip Authorization and cookies from requests to public endpoints before they reach the cache (and the origin), so the CDN does not bypass caching and the origin cannot accidentally personalise.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Map cacheable POST queries to GET\",\n      \"text\": \"For GraphQL, use persisted queries sent as GET (\u002Fgraphql?id=&variables=...), which CDNs can cache by URL.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Purge by tag when data changes\",\n      \"text\": \"When a product changes, purge product- so every cached response that includes it (detail, listings, search) is refreshed.\"\n    }\n  ]\n}\n",[1005,1010,1011],{"type":1007},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Cache API Responses at the CDN\",\n  \"description\": \"Which API responses can be cached at the CDN, how to set headers and keys for them, and how to purge them when data changes.\",\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\u002Fcdn-edge-caching-configuration\u002Fcaching-api-responses-at-the-cdn\u002F\"\n  }\n}\n",[1005,1013,1014],{"type":1007},"\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\": \"CDN Edge Caching Configuration\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Caching API Responses at the CDN\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fcaching-api-responses-at-the-cdn\u002F\"\n    }\n  ]\n}\n",[1016,1017,1018],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}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":363,"searchDepth":377,"depth":377,"links":1020},[1021,1022,1023,1029,1030,1031,1032,1033,1034],{"id":199,"depth":377,"text":200},{"id":251,"depth":377,"text":252},{"id":350,"depth":377,"text":351,"children":1024},[1025,1026,1027,1028],{"id":355,"depth":422,"text":356},{"id":551,"depth":422,"text":552},{"id":561,"depth":422,"text":562},{"id":675,"depth":422,"text":676},{"id":805,"depth":377,"text":806},{"id":812,"depth":377,"text":813},{"id":827,"depth":377,"text":828},{"id":868,"depth":377,"text":869},{"id":896,"depth":377,"text":897},{"id":979,"depth":377,"text":980},"Which API responses can be cached at the CDN, how to set headers and keys for them, and how to purge them when data changes.","md",{"slug":1038,"type":1039,"breadcrumb":1040,"datePublished":1047,"dateModified":1047},"caching-api-responses-at-the-cdn","article",[1041,1043,1044,1045],{"name":1042,"url":229},"Home",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1046},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fcaching-api-responses-at-the-cdn\u002F","2026-10-06",true,"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fcaching-api-responses-at-the-cdn",{"title":1051,"description":1052},"Caching API Responses at the CDN Safely","Cache public GET API responses at the edge with s-maxage, stale-while-revalidate and surrogate keys, keep private data uncached, and cut client data latency.","advanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fcaching-api-responses-at-the-cdn\u002Findex","emfmiF4xooY4ixXf-NCz6Tsl6PP1ZvoGqfIOHoayWQc",[1056,1059],{"title":22,"path":1057,"stem":1058,"children":-1},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration","advanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Findex",{"title":1060,"path":1061,"stem":1062,"children":-1},"Configuring stale-if-error for origin outages","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fconfiguring-stale-if-error-for-origin-outages","advanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fconfiguring-stale-if-error-for-origin-outages\u002Findex",1791308074970]