[{"data":1,"prerenderedAt":1012},["ShallowReactive",2],{"en-post-\u002Fen\u002Fhttp-cache-control-guide":3},{"id":4,"title":5,"body":6,"description":999,"extension":1000,"meta":1001,"navigation":246,"path":1007,"seo":1008,"sitemap":1009,"stem":1010,"__hash__":1011},"blogEn\u002Fen\u002Fhttp-cache-control-guide.md","Using HTTP Caching Correctly: Cache-Control, ETag, and no-cache vs no-store",{"type":7,"value":8,"toc":988},"minimark",[9,14,23,38,41,45,51,61,71,144,161,163,167,173,204,466,475,477,481,575,599,601,605,611,701,706,715,733,736,738,742,748,775,781,875,898,900,904,978,984],[10,11,13],"h2",{"id":12},"you-deployed-why-do-users-still-see-the-old-page","You deployed. Why do users still see the old page?",[15,16,17,18,22],"p",{},"The deploy is done, but users still see the old screen. Or the opposite: you added ",[19,20,21],"code",{},"no-cache"," to \"stop caching,\" yet the server logs show fewer requests than you expected. Both come from misunderstanding HTTP caching.",[24,25,26],"blockquote",{},[15,27,28,29,33,34,37],{},"HTTP caching means ",[30,31,32],"strong",{},"storing a response and reusing it",", and the rules come from the ",[19,35,36],{},"Cache-Control"," header the server sends. Caches live in the browser and in CDNs in between.",[39,40],"hr",{},[10,42,44],{"id":43},"send-the-same-request-twice","Send the same request twice",[15,46,47,48,50],{},"I served the same JSON while changing only ",[19,49,36],{},", and requested the same URL twice from a browser (Chromium). The server log records only requests that actually reached the server.",[52,53,58],"pre",{"className":54,"code":56,"language":57},[55],"language-text","### Cache-Control: max-age=60\n[server] \u002Fmax-age  -> 200          ← only the first one reaches the server\n                                    ← the second never arrives (served from the browser cache)\n\n### Cache-Control: no-cache\n[server] \u002Fno-cache  -> 200\n[server] \u002Fno-cache  if-none-match=\"661f9b33...\"  -> 304   ← the second just asks \"has it changed?\"\n\n### Cache-Control: no-store\n[server] \u002Fno-store  -> 200\n[server] \u002Fno-store  -> 200         ← fetched from scratch every time\n","text",[19,59,56],{"__ignoreMap":60},"",[15,62,63,64,67,68],{},"In all three cases, JavaScript got a ",[19,65,66],{},"200"," with the same body both times. ",[30,69,70],{},"The difference is whether the request reached the server and whether the body was downloaded again.",[72,73,74,90],"table",{},[75,76,77],"thead",{},[78,79,80,84,87],"tr",{},[81,82,83],"th",{},"Setting",[81,85,86],{},"Second request",[81,88,89],{},"Meaning",[91,92,93,110,128],"tbody",{},[78,94,95,101,107],{},[96,97,98],"td",{},[19,99,100],{},"max-age=60",[96,102,103,106],{},[30,104,105],{},"Doesn't reach"," the server",[96,108,109],{},"For 60 seconds, use the stored copy without asking",[78,111,112,116,125],{},[96,113,114],{},[19,115,21],{},[96,117,118,121,122],{},[30,119,120],{},"Only checks"," with the server → ",[19,123,124],{},"304",[96,126,127],{},"Store it, but confirm it's current every time you use it",[78,129,130,135,141],{},[96,131,132],{},[19,133,134],{},"no-store",[96,136,137,138],{},"Downloads ",[30,139,140],{},"everything again",[96,142,143],{},"Don't store it at all",[145,146,147],"warning",{},[24,148,149],{},[15,150,151,152,157,158,160],{},"This is the most confusing part because of the name. ",[30,153,154,156],{},[19,155,21],{}," doesn't mean \"don't cache\"; it means \"don't use it without checking.\""," If you really need to prevent storage (personal data, payment info), use ",[19,159,134],{},".",[39,162],{},[10,164,166],{"id":165},"etag-and-304-download-again-only-when-it-changed","ETag and 304: download again only when it changed",[15,168,169,170,172],{},"What happened on the second ",[19,171,21],{}," request is a conditional request.",[174,175,176,188,195],"ol",{},[177,178,179,180,183,184,187],"li",{},"The server sends an ",[19,181,182],{},"ETag",", a fingerprint of the content, with the first response. (",[19,185,186],{},"ETag: \"661f9b33...\"",")",[177,189,190,191,194],{},"On the next request, the browser asks with ",[19,192,193],{},"If-None-Match: \"661f9b33...\"",": \"I have this version; has it changed?\"",[177,196,197,198,203],{},"If it hasn't, the server sends just ",[30,199,200],{},[19,201,202],{},"304 Not Modified"," with no body, and the browser uses its stored copy.",[52,205,209],{"className":206,"code":207,"language":208,"meta":60,"style":60},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F Handling ETag in Node.js\nimport crypto from \"node:crypto\";\n\nconst body = JSON.stringify(posts);\nconst etag = `\"${crypto.createHash(\"sha1\").update(body).digest(\"hex\").slice(0, 16)}\"`;\n\nres.setHeader(\"Cache-Control\", \"no-cache\");\nres.setHeader(\"ETag\", etag);\n\nif (req.headers[\"if-none-match\"] === etag) {\n  res.statusCode = 304; \u002F\u002F no body: \"you can keep using yours\"\n  res.end();\n  return;\n}\nres.end(body);\n","js",[19,210,211,220,241,248,273,344,349,371,386,391,412,430,442,450,456],{"__ignoreMap":60},[212,213,216],"span",{"class":214,"line":215},"line",1,[212,217,219],{"class":218},"sJ8bj","\u002F\u002F Handling ETag in Node.js\n",[212,221,223,227,231,234,238],{"class":214,"line":222},2,[212,224,226],{"class":225},"szBVR","import",[212,228,230],{"class":229},"sVt8B"," crypto ",[212,232,233],{"class":225},"from",[212,235,237],{"class":236},"sZZnC"," \"node:crypto\"",[212,239,240],{"class":229},";\n",[212,242,244],{"class":214,"line":243},3,[212,245,247],{"emptyLinePlaceholder":246},true,"\n",[212,249,251,254,258,261,264,266,270],{"class":214,"line":250},4,[212,252,253],{"class":225},"const",[212,255,257],{"class":256},"sj4cs"," body",[212,259,260],{"class":225}," =",[212,262,263],{"class":256}," JSON",[212,265,160],{"class":229},[212,267,269],{"class":268},"sScJk","stringify",[212,271,272],{"class":229},"(posts);\n",[212,274,276,278,281,283,286,289,291,294,297,300,303,306,308,311,313,316,318,321,323,326,328,331,334,337,339,342],{"class":214,"line":275},5,[212,277,253],{"class":225},[212,279,280],{"class":256}," etag",[212,282,260],{"class":225},[212,284,285],{"class":236}," `\"${",[212,287,288],{"class":229},"crypto",[212,290,160],{"class":236},[212,292,293],{"class":268},"createHash",[212,295,296],{"class":236},"(",[212,298,299],{"class":236},"\"sha1\"",[212,301,302],{"class":236},").",[212,304,305],{"class":268},"update",[212,307,296],{"class":236},[212,309,310],{"class":229},"body",[212,312,302],{"class":236},[212,314,315],{"class":268},"digest",[212,317,296],{"class":236},[212,319,320],{"class":236},"\"hex\"",[212,322,302],{"class":236},[212,324,325],{"class":268},"slice",[212,327,296],{"class":236},[212,329,330],{"class":256},"0",[212,332,333],{"class":236},", ",[212,335,336],{"class":256},"16",[212,338,187],{"class":236},[212,340,341],{"class":236},"}\"`",[212,343,240],{"class":229},[212,345,347],{"class":214,"line":346},6,[212,348,247],{"emptyLinePlaceholder":246},[212,350,352,355,358,360,363,365,368],{"class":214,"line":351},7,[212,353,354],{"class":229},"res.",[212,356,357],{"class":268},"setHeader",[212,359,296],{"class":229},[212,361,362],{"class":236},"\"Cache-Control\"",[212,364,333],{"class":229},[212,366,367],{"class":236},"\"no-cache\"",[212,369,370],{"class":229},");\n",[212,372,374,376,378,380,383],{"class":214,"line":373},8,[212,375,354],{"class":229},[212,377,357],{"class":268},[212,379,296],{"class":229},[212,381,382],{"class":236},"\"ETag\"",[212,384,385],{"class":229},", etag);\n",[212,387,389],{"class":214,"line":388},9,[212,390,247],{"emptyLinePlaceholder":246},[212,392,394,397,400,403,406,409],{"class":214,"line":393},10,[212,395,396],{"class":225},"if",[212,398,399],{"class":229}," (req.headers[",[212,401,402],{"class":236},"\"if-none-match\"",[212,404,405],{"class":229},"] ",[212,407,408],{"class":225},"===",[212,410,411],{"class":229}," etag) {\n",[212,413,415,418,421,424,427],{"class":214,"line":414},11,[212,416,417],{"class":229},"  res.statusCode ",[212,419,420],{"class":225},"=",[212,422,423],{"class":256}," 304",[212,425,426],{"class":229},"; ",[212,428,429],{"class":218},"\u002F\u002F no body: \"you can keep using yours\"\n",[212,431,433,436,439],{"class":214,"line":432},12,[212,434,435],{"class":229},"  res.",[212,437,438],{"class":268},"end",[212,440,441],{"class":229},"();\n",[212,443,445,448],{"class":214,"line":444},13,[212,446,447],{"class":225},"  return",[212,449,240],{"class":229},[212,451,453],{"class":214,"line":452},14,[212,454,455],{"class":229},"}\n",[212,457,459,461,463],{"class":214,"line":458},15,[212,460,354],{"class":229},[212,462,438],{"class":268},[212,464,465],{"class":229},"(body);\n",[15,467,468,469,471,472,474],{},"The bigger the response and the slower the network (mobile), the more ",[19,470,124],{}," saves. Most web frameworks and static file servers add ",[19,473,182],{}," automatically.",[39,476],{},[10,478,480],{"id":479},"cache-control-directives-at-a-glance","Cache-Control directives at a glance",[72,482,483,492],{},[75,484,485],{},[78,486,487,490],{},[81,488,489],{},"Directive",[81,491,89],{},[91,493,494,504,513,522,532,542,555,565],{},[78,495,496,501],{},[96,497,498],{},[19,499,500],{},"max-age=seconds",[96,502,503],{},"Use the stored copy without asking the server for this long",[78,505,506,510],{},[96,507,508],{},[19,509,21],{},[96,511,512],{},"Store it, but check with the server every time before using it",[78,514,515,519],{},[96,516,517],{},[19,518,134],{},[96,520,521],{},"Don't store it anywhere",[78,523,524,529],{},[96,525,526],{},[19,527,528],{},"private",[96,530,531],{},"Browser only. Shared caches like CDNs must not store it (per-user data)",[78,533,534,539],{},[96,535,536],{},[19,537,538],{},"public",[96,540,541],{},"Shared caches (CDNs) may store it too",[78,543,544,549],{},[96,545,546],{},[19,547,548],{},"s-maxage=seconds",[96,550,551,554],{},[19,552,553],{},"max-age"," for shared caches (CDNs) only. Browsers ignore it",[78,556,557,562],{},[96,558,559],{},[19,560,561],{},"immutable",[96,563,564],{},"It won't change while fresh, so don't revalidate even on reload",[78,566,567,572],{},[96,568,569],{},[19,570,571],{},"stale-while-revalidate=seconds",[96,573,574],{},"Serve the expired copy right away and fetch a fresh one in the background",[576,577,578],"note",{},[24,579,580],{},[15,581,582,583,585,586,589,590,593,594,596,597,302],{},"Put ",[19,584,538],{}," or ",[19,587,588],{},"s-maxage"," on a logged-in user's data (a \"my account\" API, for example) and the CDN can serve ",[30,591,592],{},"one user's response to another user",". For responses that differ per user, use ",[19,595,528],{}," (or ",[19,598,134],{},[39,600],{},[10,602,604],{"id":603},"practical-combinations-different-settings-per-file-type","Practical combinations: different settings per file type",[15,606,607,608],{},"The key to cache settings is matching ",[30,609,610],{},"\"when does this response change?\"",[72,612,613,626],{},[75,614,615],{},[78,616,617,620,623],{},[81,618,619],{},"Target",[81,621,622],{},"Recommended",[81,624,625],{},"Why",[91,627,628,644,659,673,686],{},[78,629,630,636,641],{},[96,631,632,633,187],{},"JS\u002FCSS with a hash in the filename (",[19,634,635],{},"app.3f9a1c.js",[96,637,638],{},[19,639,640],{},"public, max-age=31536000, immutable",[96,642,643],{},"New content gets a new filename, so the same name always means the same content",[78,645,646,649,656],{},[96,647,648],{},"HTML documents",[96,650,651,653,654,187],{},[19,652,21],{}," (+ ",[19,655,182],{},[96,657,658],{},"It must point to the new deploy's new JS filenames, so always check",[78,660,661,664,670],{},[96,662,663],{},"Images (no hash in the name)",[96,665,666,667],{},"Around ",[19,668,669],{},"public, max-age=86400",[96,671,672],{},"They change occasionally, so a moderate lifetime",[78,674,675,678,683],{},[96,676,677],{},"Public API (a list of posts)",[96,679,680],{},[19,681,682],{},"public, s-maxage=60, stale-while-revalidate=300",[96,684,685],{},"The CDN answers for a minute and stays fast while refreshing",[78,687,688,691,698],{},[96,689,690],{},"Per-user API",[96,692,693,585,696],{},[19,694,695],{},"private, no-cache",[19,697,134],{},[96,699,700],{},"Must not be stored in shared caches",[702,703,705],"h3",{"id":704},"why-users-see-the-old-page-after-a-deploy","Why users see the old page after a deploy",[15,707,708,709,714],{},"Usually because ",[30,710,711,712],{},"HTML was given a long ",[19,713,553],{},". When HTML is cached, the browser uses the old HTML, and that HTML loads the old JS files. The fix is a simple formula:",[716,717,718,726],"ul",{},[177,719,720,723,724,187],{},[30,721,722],{},"Always revalidate HTML"," (",[19,725,21],{},[177,727,728,723,731,187],{},[30,729,730],{},"Hash JS\u002FCSS filenames and cache them forever",[19,732,561],{},[15,734,735],{},"Build tools like Vite, Nuxt, and Next.js hash JS\u002FCSS filenames automatically, so you only need to be careful with HTML caching.",[39,737],{},[10,739,741],{"id":740},"cdn-caching-and-vercel","CDN caching and Vercel",[15,743,744,745,747],{},"On platforms like Vercel, there's a CDN between the browser and your server. If your server (function) sends ",[19,746,588],{},", the CDN stores the response for that long and answers in your server's place. This directly affects how often your serverless functions run and how fast responses are.",[52,749,751],{"className":206,"code":750,"language":208,"meta":60,"style":60},"\u002F\u002F API response: browsers revalidate every time, the CDN answers for 60 seconds\nres.setHeader(\"Cache-Control\", \"public, max-age=0, s-maxage=60, stale-while-revalidate=300\");\n",[19,752,753,758],{"__ignoreMap":60},[212,754,755],{"class":214,"line":215},[212,756,757],{"class":218},"\u002F\u002F API response: browsers revalidate every time, the CDN answers for 60 seconds\n",[212,759,760,762,764,766,768,770,773],{"class":214,"line":222},[212,761,354],{"class":229},[212,763,357],{"class":268},[212,765,296],{"class":229},[212,767,362],{"class":236},[212,769,333],{"class":229},[212,771,772],{"class":236},"\"public, max-age=0, s-maxage=60, stale-while-revalidate=300\"",[212,774,370],{"class":229},[15,776,777,778,160],{},"In Nuxt, you can declare caching per route with ",[19,779,780],{},"routeRules",[52,782,786],{"className":783,"code":784,"language":785,"meta":60,"style":60},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  routeRules: {\n    \"\u002F\": { swr: 600 }, \u002F\u002F cache for 10 minutes; after expiry, serve the stored copy and refresh in the background\n    \"\u002Fapi\u002Fposts\": { headers: { \"cache-control\": \"public, s-maxage=60\" } },\n    \"\u002Fmypage\u002F**\": { headers: { \"cache-control\": \"private, no-store\" } },\n  },\n});\n","ts",[19,787,788,793,807,812,829,849,865,870],{"__ignoreMap":60},[212,789,790],{"class":214,"line":215},[212,791,792],{"class":218},"\u002F\u002F nuxt.config.ts\n",[212,794,795,798,801,804],{"class":214,"line":222},[212,796,797],{"class":225},"export",[212,799,800],{"class":225}," default",[212,802,803],{"class":268}," defineNuxtConfig",[212,805,806],{"class":229},"({\n",[212,808,809],{"class":214,"line":243},[212,810,811],{"class":229},"  routeRules: {\n",[212,813,814,817,820,823,826],{"class":214,"line":250},[212,815,816],{"class":236},"    \"\u002F\"",[212,818,819],{"class":229},": { swr: ",[212,821,822],{"class":256},"600",[212,824,825],{"class":229}," }, ",[212,827,828],{"class":218},"\u002F\u002F cache for 10 minutes; after expiry, serve the stored copy and refresh in the background\n",[212,830,831,834,837,840,843,846],{"class":214,"line":275},[212,832,833],{"class":236},"    \"\u002Fapi\u002Fposts\"",[212,835,836],{"class":229},": { headers: { ",[212,838,839],{"class":236},"\"cache-control\"",[212,841,842],{"class":229},": ",[212,844,845],{"class":236},"\"public, s-maxage=60\"",[212,847,848],{"class":229}," } },\n",[212,850,851,854,856,858,860,863],{"class":214,"line":346},[212,852,853],{"class":236},"    \"\u002Fmypage\u002F**\"",[212,855,836],{"class":229},[212,857,839],{"class":236},[212,859,842],{"class":229},[212,861,862],{"class":236},"\"private, no-store\"",[212,864,848],{"class":229},[212,866,867],{"class":214,"line":351},[212,868,869],{"class":229},"  },\n",[212,871,872],{"class":214,"line":373},[212,873,874],{"class":229},"});\n",[876,877,878],"tip",{},[24,879,880],{},[15,881,882,883,333,885,333,887,890,891,894,895,897],{},"After changing cache settings, check the response headers yourself in the dev tools Network tab. ",[19,884,36],{},[19,886,182],{},[19,888,889],{},"Age"," (how many seconds the CDN has held it), and markers like ",[19,892,893],{},"(disk cache)"," and ",[19,896,124],{}," show what's really happening.",[39,899],{},[10,901,903],{"id":902},"summary-why-this-is-worth-knowing","Summary: why this is worth knowing",[72,905,906,916],{},[75,907,908],{},[78,909,910,913],{},[81,911,912],{},"Misconception \u002F problem",[81,914,915],{},"Reality \u002F fix",[91,917,918,930,940,953,965],{},[78,919,920,925],{},[96,921,922,924],{},[19,923,21],{}," means no caching",[96,926,927,928],{},"It means store and check every time. No storing is ",[19,929,134],{},[78,931,932,935],{},[96,933,934],{},"Old page after a deploy",[96,936,937,939],{},[19,938,21],{}," for HTML, long caching only for hashed JS\u002FCSS",[78,941,942,945],{},[96,943,944],{},"Downloading the full response every time",[96,946,947,949,950,952],{},[19,948,182],{}," + ",[19,951,124],{}," to download only when it changed",[78,954,955,958],{},[96,956,957],{},"Personal data shown to someone else",[96,959,960,585,962,964],{},[19,961,528],{},[19,963,134],{}," for per-user responses",[78,966,967,970],{},[96,968,969],{},"Heavy server (function) load",[96,971,972,949,974,977],{},[19,973,588],{},[19,975,976],{},"stale-while-revalidate"," on public responses",[15,979,980,981],{},"Used well, caching cuts server cost and response time at once. Used badly, it causes \"I fixed it but it's not fixed\" bugs and privacy incidents. Before adding a header, ask two questions: ",[30,982,983],{},"\"When does this response change, and is it the same for everyone?\"",[985,986,987],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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);}",{"title":60,"searchDepth":222,"depth":222,"links":989},[990,991,992,993,994,997,998],{"id":12,"depth":222,"text":13},{"id":43,"depth":222,"text":44},{"id":165,"depth":222,"text":166},{"id":479,"depth":222,"text":480},{"id":603,"depth":222,"text":604,"children":995},[996],{"id":704,"depth":243,"text":705},{"id":740,"depth":222,"text":741},{"id":902,"depth":222,"text":903},"no-cache doesn't mean 'don't cache.' How max-age, no-cache, no-store, s-maxage, and immutable actually behave, checked by counting how many requests reach the server from a real browser, plus ETag and 304, old files showing up after a deploy, and Vercel's CDN cache.","md",{"date":1002,"field":1003,"tags":1004},"2026.10.08","backend",[1005,1006],"http","vercel","\u002Fen\u002Fhttp-cache-control-guide",{"title":5,"description":999},{"loc":1007},"en\u002Fhttp-cache-control-guide","baXwAc6cepkpwIL7yODHkTVd30lFw9la2kyyugdDN6M",1791482015983]