[{"data":1,"prerenderedAt":1072},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-with-nginx-proxy-cache":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-with-nginx-proxy-cache":1063},{"id":4,"title":5,"body":6,"description":1045,"extension":1046,"meta":1047,"navigation":371,"path":1058,"seo":1059,"stem":1061,"__hash__":1062},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-with-nginx-proxy-cache\u002Findex.md","Stale-While-Revalidate with nginx proxy_cache",{"type":7,"value":8,"toc":1027},"minimark",[9,14,46,49,188,193,232,236,242,248,254,271,337,341,346,524,528,588,591,595,610,614,678,783,787,794,798,808,812,815,819,826,830,864,868,882,892,901,907,911,926,935,944,964,973,985,989,1012,1017,1020,1023],[10,11,13],"h1",{"id":12},"how-to-implement-stale-while-revalidate-with-nginx-proxy_cache","How to Implement Stale-While-Revalidate with nginx proxy_cache",[15,16,17,18,23,24,28,29,33,34,37,38,41,42,45],"p",{},"This guide implements ",[19,20,22],"a",{"href":21},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002F","Stale-While-Revalidate Implementation"," at the origin, within ",[19,25,27],{"href":26},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",". Not every stack sits behind a CDN that caches HTML, and even those that do benefit from a cache in front of the application servers: it absorbs CDN misses, revalidations from many edge locations, and traffic that bypasses the CDN. nginx's ",[30,31,32],"code",{},"proxy_cache"," provides exactly that, with three features that together implement stale-while-revalidate: ",[30,35,36],{},"proxy_cache_use_stale updating",", ",[30,39,40],{},"proxy_cache_background_update",", and ",[30,43,44],{},"proxy_cache_lock",".",[15,47,48],{},"Configured correctly, nginx returns a cached (possibly stale) response immediately, refreshes it with a single background request, collapses concurrent misses into one upstream fetch, and keeps serving stale content when the application is down. Application servers see a small, steady stream of refreshes instead of every request, and TTFB for cached pages falls to a few milliseconds of nginx processing.",[15,50,51],{},[52,53,59,60,59,67,59,71,59,74,59,92,59,100,59,106,59,113,59,120,59,123,59,127,59,130,59,134,59,143,59,146,59,149,59,155,59,161,59,166,59,170,59,174,59,179,59,184,59],"svg",{"viewBox":54,"width":55,"role":56,"ariaLabel":57,"style":58},"0 0 760 290","100%","img","Sequence of a client receiving a stale response immediately from nginx while nginx sends one background request to refresh the cache.","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",{},"nginx serving stale while updating in the background",[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},"fab75a0786","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","288","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":108,"y":109,"width":110,"height":104,"rx":111,"fill":90,"stroke":90,"style":112},"70.3","56.0","150.0","6","fill-opacity:0.06;stroke-opacity:0.4",[101,114,119],{"x":115,"y":116,"fill":90,"style":117,"textAnchor":118},"145.3","78.0","font-size:13px;font-weight:700","middle","Client",[61,121],{"x":122,"y":109,"width":110,"height":104,"rx":111,"fill":90,"stroke":90,"style":112},"305.0",[101,124,126],{"x":125,"y":116,"fill":90,"style":117,"textAnchor":118},"380.0","nginx",[61,128],{"x":129,"y":109,"width":110,"height":104,"rx":111,"fill":90,"stroke":90,"style":112},"539.7",[101,131,133],{"x":132,"y":116,"fill":90,"style":117,"textAnchor":118},"614.7","App server",[135,136],"line",{"x1":115,"y1":137,"x2":115,"y2":138,"stroke":90,"strokeWidth":139,"strokeDashArray":140,"style":142},"90.0","268.0","1.5",[141,141],"4","stroke-opacity:0.35",[135,144],{"x1":125,"y1":137,"x2":125,"y2":138,"stroke":90,"strokeWidth":139,"strokeDashArray":145,"style":142},[141,141],[135,147],{"x1":132,"y1":137,"x2":132,"y2":138,"stroke":90,"strokeWidth":139,"strokeDashArray":148,"style":142},[141,141],[135,150],{"x1":115,"y1":151,"x2":152,"y2":151,"stroke":90,"strokeWidth":139,"style":153,"markerEnd":154},"124.0","378.0","stroke-opacity:0.6","url(#fab75a0786)",[101,156,160],{"x":157,"y":158,"fill":90,"style":159,"textAnchor":118},"262.7","117.0","font-size:12px","GET \u002Fproducts\u002F42",[135,162],{"x1":125,"y1":163,"x2":164,"y2":163,"stroke":90,"strokeWidth":139,"strokeDashArray":165,"style":153,"markerEnd":154},"164.0","147.3",[83,141],[101,167,169],{"x":157,"y":168,"fill":90,"style":159,"textAnchor":118},"157.0","STALE response (instant)",[135,171],{"x1":125,"y1":172,"x2":173,"y2":172,"stroke":90,"strokeWidth":139,"style":153,"markerEnd":154},"204.0","612.7",[101,175,178],{"x":176,"y":177,"fill":90,"style":159,"textAnchor":118},"497.3","197.0","background update",[135,180],{"x1":132,"y1":181,"x2":182,"y2":181,"stroke":90,"strokeWidth":139,"strokeDashArray":183,"style":153,"markerEnd":154},"244.0","382.0",[83,141],[101,185,187],{"x":176,"y":186,"fill":90,"style":159,"textAnchor":118},"237.0","fresh 200",[189,190,192],"h2",{"id":191},"rapid-diagnosis","Rapid Diagnosis",[194,195,196,211,217,226],"ul",{},[197,198,199,203,204,206,207,210],"li",{},[200,201,202],"strong",{},"Check whether nginx caches at all."," Look for ",[30,205,32],{}," directives; add ",[30,208,209],{},"add_header X-Cache-Status $upstream_cache_status;"," to see HIT, MISS, EXPIRED, STALE, UPDATING.",[197,212,213,216],{},[200,214,215],{},"Watch upstream request rates during traffic spikes."," If they track client request rates, there is no effective cache.",[197,218,219,222,223,225],{},[200,220,221],{},"Check for thundering herds."," After expiry, do many simultaneous requests reach the app? Without ",[30,224,44],{}," they will.",[197,227,228,231],{},[200,229,230],{},"Check failure behaviour."," Stop the app in staging; do cached pages still serve?",[189,233,235],{"id":234},"root-cause-analysis","Root Cause Analysis",[15,237,238,241],{},[200,239,240],{},"1. No caching for HTML."," nginx proxies everything to the app by default.",[15,243,244,247],{},[200,245,246],{},"2. Blocking revalidation."," Without background update, the request that finds an expired entry waits for the upstream.",[15,249,250,253],{},[200,251,252],{},"3. Concurrent misses."," Without a lock, every request arriving during a miss goes upstream.",[15,255,256,259,260,263,264,267,268,45],{},[200,257,258],{},"4. Cookies and headers preventing caching."," nginx does not cache responses with ",[30,261,262],{},"Set-Cookie"," by default, and respects upstream ",[30,265,266],{},"Cache-Control: private","\u002F",[30,269,270],{},"no-store",[15,272,273],{},[52,274,59,277,59,280,59,283,59,285,59,288,59,290,59,297,59,306,59,311,59,315,59,319,59,323,59,327,59,333,59],{"viewBox":275,"width":55,"role":56,"ariaLabel":276,"style":58},"0 0 760 163","Bar chart of requests reaching application servers per second during a spike under different nginx cache configurations.",[61,278],{"className":279,"x":65,"y":65,"width":55,"height":55,"fill":66},[64],[68,281,282],{},"Upstream requests per second during a traffic spike",[72,284,276],{},[61,286],{"x":94,"y":94,"width":95,"height":287,"rx":97,"fill":98,"stroke":90,"style":99},"161",[101,289,282],{"x":103,"y":104,"fill":90,"style":105},[101,291,296],{"x":292,"y":293,"fill":90,"style":294,"textAnchor":295},"185.7","70.0","font-size:13px","end","No proxy_cache",[61,298],{"x":299,"y":109,"width":300,"height":301,"rx":302,"fill":303,"stroke":304,"style":305},"197.7","470.3","19","3","#ffc300","#b8860b","fill-opacity:0.7;stroke-opacity:0.9",[101,307,310],{"x":308,"y":293,"fill":90,"style":309},"674.0","font-size:12px;font-weight:600","1200 rps",[101,312,314],{"x":292,"y":313,"fill":90,"style":294,"textAnchor":295},"101.0","proxy_cache, no lock",[61,316],{"x":299,"y":317,"width":318,"height":301,"rx":302,"fill":303,"stroke":304,"style":305},"87.0","121.5",[101,320,322],{"x":321,"y":313,"fill":90,"style":309},"325.2","310 rps",[101,324,326],{"x":292,"y":325,"fill":90,"style":294,"textAnchor":295},"132.0","+ lock + use_stale updating",[61,328],{"x":299,"y":329,"width":330,"height":301,"rx":302,"fill":331,"stroke":331,"style":332},"118.0","7.1","#0466c8","fill-opacity:0.55;stroke-opacity:0.9",[101,334,336],{"x":335,"y":325,"fill":90,"style":309},"210.7","18 rps",[189,338,340],{"id":339},"step-by-step-resolution","Step-by-Step Resolution",[342,343,345],"h3",{"id":344},"_1-define-a-cache-zone-and-enable-caching-for-public-routes","1. Define a cache zone and enable caching for public routes",[347,348,352],"pre",{"className":349,"code":350,"language":126,"meta":351,"style":351},"language-nginx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","proxy_cache_path \u002Fvar\u002Fcache\u002Fnginx\u002Fhtml levels=1:2 keys_zone=html:50m max_size=2g inactive=7d use_temp_path=off;\n\nserver {\n  location \u002F {\n    proxy_pass http:\u002F\u002Fapp;\n    proxy_cache html;\n    proxy_cache_key \"$scheme$host$uri$is_args$args\";\n    proxy_cache_valid 200 301 10m;\n    proxy_cache_valid 404 1m;\n    add_header X-Cache-Status $upstream_cache_status always;\n    # trade-off: proxy_cache_valid only applies when the upstream sends no\n    # Cache-Control\u002FExpires; if the app sends headers, nginx follows them unless\n    # proxy_ignore_headers is set. Decide which side owns the TTL.\n  }\n}\n","",[30,353,354,366,373,382,395,404,413,453,471,484,493,500,506,512,518],{"__ignoreMap":351},[355,356,358,362],"span",{"class":135,"line":357},1,[355,359,361],{"class":360},"sPARh","proxy_cache_path ",[355,363,365],{"class":364},"saISM","\u002Fvar\u002Fcache\u002Fnginx\u002Fhtml levels=1:2 keys_zone=html:50m max_size=2g inactive=7d use_temp_path=off;\n",[355,367,369],{"class":135,"line":368},2,[355,370,372],{"emptyLinePlaceholder":371},true,"\n",[355,374,376,379],{"class":135,"line":375},3,[355,377,378],{"class":360},"server",[355,380,381],{"class":364}," {\n",[355,383,385,388,392],{"class":135,"line":384},4,[355,386,387],{"class":360},"  location",[355,389,391],{"class":390},"sQw3B"," \u002F ",[355,393,394],{"class":364},"{\n",[355,396,398,401],{"class":135,"line":397},5,[355,399,400],{"class":360},"    proxy_pass ",[355,402,403],{"class":364},"http:\u002F\u002Fapp;\n",[355,405,407,410],{"class":135,"line":406},6,[355,408,409],{"class":360},"    proxy_cache ",[355,411,412],{"class":364},"html;\n",[355,414,416,419,423,426,429,432,434,437,439,442,444,447,450],{"class":135,"line":415},7,[355,417,418],{"class":360},"    proxy_cache_key ",[355,420,422],{"class":421},"sZ8jY","\"$",[355,424,425],{"class":364},"scheme",[355,427,428],{"class":421},"$",[355,430,431],{"class":364},"host",[355,433,428],{"class":421},[355,435,436],{"class":364},"uri",[355,438,428],{"class":421},[355,440,441],{"class":364},"is_args",[355,443,428],{"class":421},[355,445,446],{"class":364},"args",[355,448,449],{"class":421},"\"",[355,451,452],{"class":364},";\n",[355,454,456,459,463,466,469],{"class":135,"line":455},8,[355,457,458],{"class":360},"    proxy_cache_valid ",[355,460,462],{"class":461},"sPXB4","200",[355,464,465],{"class":461}," 301",[355,467,468],{"class":461}," 10m",[355,470,452],{"class":364},[355,472,474,476,479,482],{"class":135,"line":473},9,[355,475,458],{"class":360},[355,477,478],{"class":461},"404",[355,480,481],{"class":461}," 1m",[355,483,452],{"class":364},[355,485,487,490],{"class":135,"line":486},10,[355,488,489],{"class":360},"    add_header ",[355,491,492],{"class":364},"X-Cache-Status $upstream_cache_status always;\n",[355,494,496],{"class":135,"line":495},11,[355,497,499],{"class":498},"sjfSM","    # trade-off: proxy_cache_valid only applies when the upstream sends no\n",[355,501,503],{"class":135,"line":502},12,[355,504,505],{"class":498},"    # Cache-Control\u002FExpires; if the app sends headers, nginx follows them unless\n",[355,507,509],{"class":135,"line":508},13,[355,510,511],{"class":498},"    # proxy_ignore_headers is set. Decide which side owns the TTL.\n",[355,513,515],{"class":135,"line":514},14,[355,516,517],{"class":364},"  }\n",[355,519,521],{"class":135,"line":520},15,[355,522,523],{"class":364},"}\n",[342,525,527],{"id":526},"_2-serve-stale-while-updating-in-the-background","2. Serve stale while updating, in the background",[347,529,531],{"className":349,"code":530,"language":126,"meta":351,"style":351},"proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;\nproxy_cache_background_update on;\nproxy_cache_lock on;\nproxy_cache_lock_timeout 5s;\n# trade-off: 'updating' serves stale indefinitely while an update is in flight;\n# if the upstream hangs, clients keep receiving the old version until the\n# update completes or times out. Keep upstream timeouts sensible.\n",[30,532,533,544,554,563,573,578,583],{"__ignoreMap":351},[355,534,535,538,541],{"class":135,"line":357},[355,536,537],{"class":360},"proxy_cache_use_stale ",[355,539,540],{"class":461},"error",[355,542,543],{"class":364}," timeout updating http_500 http_502 http_503 http_504;\n",[355,545,546,549,552],{"class":135,"line":368},[355,547,548],{"class":360},"proxy_cache_background_update ",[355,550,551],{"class":461},"on",[355,553,452],{"class":364},[355,555,556,559,561],{"class":135,"line":375},[355,557,558],{"class":360},"proxy_cache_lock ",[355,560,551],{"class":461},[355,562,452],{"class":364},[355,564,565,568,571],{"class":135,"line":384},[355,566,567],{"class":360},"proxy_cache_lock_timeout ",[355,569,570],{"class":461},"5s",[355,572,452],{"class":364},[355,574,575],{"class":135,"line":397},[355,576,577],{"class":498},"# trade-off: 'updating' serves stale indefinitely while an update is in flight;\n",[355,579,580],{"class":135,"line":406},[355,581,582],{"class":498},"# if the upstream hangs, clients keep receiving the old version until the\n",[355,584,585],{"class":135,"line":415},[355,586,587],{"class":498},"# update completes or times out. Keep upstream timeouts sensible.\n",[15,589,590],{},"Expected outcome: expired entries are served instantly while one request refreshes them; concurrent misses wait for a single upstream fetch.",[342,592,594],{"id":593},"_3-honour-stale-while-revalidate-from-the-upstream","3. Honour stale-while-revalidate from the upstream",[15,596,597,598,601,602,605,606,609],{},"nginx understands ",[30,599,600],{},"stale-while-revalidate"," and ",[30,603,604],{},"stale-if-error"," extensions in upstream ",[30,607,608],{},"Cache-Control"," headers (since 1.11.10), so the application can set windows per response while nginx enforces them.",[342,611,613],{"id":612},"_4-bypass-the-cache-for-personal-requests","4. Bypass the cache for personal requests",[347,615,617],{"className":349,"code":616,"language":126,"meta":351,"style":351},"map $http_cookie $skip_cache { default 0; ~*session= 1; }\nproxy_cache_bypass $skip_cache;\nproxy_no_cache $skip_cache;\n# trade-off: bypassing on a session cookie keeps logged-in traffic uncached.\n# Move to a shared-shell architecture to cache those users too.\n",[30,618,619,653,661,668,673],{"__ignoreMap":351},[355,620,621,624,627,630,633,636,639,642,645,648,650],{"class":135,"line":357},[355,622,623],{"class":360},"map",[355,625,626],{"class":364}," $",[355,628,629],{"class":390},"http_cookie",[355,631,632],{"class":364}," $skip_cache { ",[355,634,635],{"class":461},"default",[355,637,638],{"class":461}," 0",[355,640,641],{"class":364},"; ",[355,643,644],{"class":360},"~*",[355,646,647],{"class":364},"session= ",[355,649,94],{"class":461},[355,651,652],{"class":364},"; }\n",[355,654,655,658],{"class":135,"line":368},[355,656,657],{"class":360},"proxy_cache_bypass ",[355,659,660],{"class":364},"$skip_cache;\n",[355,662,663,666],{"class":135,"line":375},[355,664,665],{"class":360},"proxy_no_cache ",[355,667,660],{"class":364},[355,669,670],{"class":135,"line":384},[355,671,672],{"class":498},"# trade-off: bypassing on a session cookie keeps logged-in traffic uncached.\n",[355,674,675],{"class":135,"line":397},[355,676,677],{"class":498},"# Move to a shared-shell architecture to cache those users too.\n",[15,679,680],{},[52,681,59,683,59,686,59,689,59,691,59,693,59,695,59,699,59,706,59,710,59,714,59,717,59,720,59,723,59,726,59,729,59,732,59,734,59,737,59,740,59,743,59,745,59,748,59,752,59,756,59,760,59,762,59,766,59,769,59,773,59,777,59,779,59],{"viewBox":54,"width":55,"role":56,"ariaLabel":682,"style":58},"The nginx proxy_cache directives that together implement stale-while-revalidate and what each contributes.",[61,684],{"className":685,"x":65,"y":65,"width":55,"height":55,"fill":66},[64],[68,687,688],{},"nginx directives that make up SWR",[72,690,682],{},[61,692],{"x":94,"y":94,"width":95,"height":96,"rx":97,"fill":98,"stroke":90,"style":99},[101,694,688],{"x":103,"y":104,"fill":90,"style":105},[61,696],{"x":103,"y":109,"width":697,"height":698,"rx":65,"fill":90,"stroke":90,"style":112},"210.0","30.0",[101,700,705],{"x":701,"y":702,"fill":90,"style":703,"textAnchor":704},"38.0","75.5","font-size:12.5px;font-weight:700","start","Directive",[61,707],{"x":708,"y":109,"width":709,"height":698,"rx":65,"fill":90,"stroke":90,"style":112},"238.0","494.0",[101,711,713],{"x":712,"y":702,"fill":90,"style":703,"textAnchor":118},"485.0","Effect",[61,715],{"x":103,"y":716,"width":697,"height":698,"rx":65,"fill":98,"stroke":90,"style":142},"86.0",[101,718,36],{"x":701,"y":719,"fill":90,"style":703,"textAnchor":704},"105.5",[61,721],{"x":708,"y":716,"width":709,"height":698,"rx":65,"fill":331,"stroke":331,"style":722},"fill-opacity:0.14;stroke-opacity:0.9",[101,724,725],{"x":712,"y":719,"fill":90,"style":159,"textAnchor":118},"serve stale while an update is in progress",[61,727],{"x":103,"y":728,"width":697,"height":698,"rx":65,"fill":98,"stroke":90,"style":142},"116.0",[101,730,40],{"x":701,"y":731,"fill":90,"style":703,"textAnchor":704},"135.5",[61,733],{"x":708,"y":728,"width":709,"height":698,"rx":65,"fill":331,"stroke":331,"style":722},[101,735,736],{"x":712,"y":731,"fill":90,"style":159,"textAnchor":118},"refresh without blocking the client",[61,738],{"x":103,"y":739,"width":697,"height":698,"rx":65,"fill":98,"stroke":90,"style":142},"146.0",[101,741,44],{"x":701,"y":742,"fill":90,"style":703,"textAnchor":704},"165.5",[61,744],{"x":708,"y":739,"width":709,"height":698,"rx":65,"fill":331,"stroke":331,"style":722},[101,746,747],{"x":712,"y":742,"fill":90,"style":159,"textAnchor":118},"one upstream fetch per key on misses",[61,749],{"x":103,"y":750,"width":697,"height":751,"rx":65,"fill":98,"stroke":90,"style":142},"176.0","46.0",[101,753,755],{"x":701,"y":754,"fill":90,"style":703,"textAnchor":704},"195.5","proxy_cache_use_stale error",[101,757,759],{"x":701,"y":758,"fill":90,"style":703,"textAnchor":704},"211.5","timeout 5xx",[61,761],{"x":708,"y":750,"width":709,"height":751,"rx":65,"fill":331,"stroke":331,"style":722},[101,763,765],{"x":712,"y":764,"fill":90,"style":159,"textAnchor":118},"203.5","stale on upstream failure",[61,767],{"x":103,"y":768,"width":697,"height":751,"rx":65,"fill":98,"stroke":90,"style":142},"222.0",[101,770,772],{"x":701,"y":771,"fill":90,"style":703,"textAnchor":704},"241.5","proxy_cache_bypass \u002F",[101,774,776],{"x":701,"y":775,"fill":90,"style":703,"textAnchor":704},"257.5","proxy_no_cache",[61,778],{"x":708,"y":768,"width":709,"height":751,"rx":65,"fill":90,"stroke":90,"style":112},[101,780,782],{"x":712,"y":781,"fill":90,"style":159,"textAnchor":118},"249.5","keep personal requests out",[189,784,786],{"id":785},"verification","Verification",[15,788,789,790,793],{},"Send repeated requests and watch ",[30,791,792],{},"X-Cache-Status",": MISS, then HIT, then (after expiry) STALE or UPDATING followed by HIT. Load-test a page while its entry expires and confirm upstream logs show a single request. Stop the application and confirm cached pages still serve with STALE status. In RUM, TTFB p75 for cached routes should drop to near network latency.",[189,795,797],{"id":796},"worked-example-a-self-hosted-cms","Worked Example: A Self-Hosted CMS",[15,799,800,801,803,804,807],{},"A self-hosted CMS rendered pages in 300–600ms and had no CDN HTML caching. A campaign drove a 15x traffic spike that overwhelmed the application servers, with TTFB p95 exceeding 8 seconds. Adding nginx ",[30,802,32],{}," with a 5-minute validity, ",[30,805,806],{},"use_stale updating",", background updates and locking (bypassing for editor sessions) reduced upstream traffic during the next spike to a few requests per second per page. TTFB p75 fell to 25ms for cached pages, and a later application crash went unnoticed by visitors for its 6-minute duration.",[189,809,811],{"id":810},"purging-nginx-caches","Purging nginx Caches",[15,813,814],{},"Open-source nginx has no built-in purge endpoint (the commercial version and third-party modules do). Common approaches: short validity combined with SWR so changes propagate within minutes; a cache key that includes a content version bumped on publish; or deleting cache files for a key via a small purge script. For many content sites, versioned keys plus SWR are the simplest reliable option.",[189,816,818],{"id":817},"monitoring-the-proxy-cache","Monitoring the Proxy Cache",[15,820,821,822,825],{},"Expose ",[30,823,824],{},"$upstream_cache_status"," in access logs alongside request time and upstream response time, then chart the share of HIT, STALE, UPDATING, MISS and BYPASS per route. A healthy configuration shows mostly HIT and STALE with occasional UPDATING; a high MISS share points to keys that are too specific or validity that is too short, and a high BYPASS share to cookies or headers excluding requests. Alert when upstream response time rises while STALE serving increases — that pattern means the cache is hiding an application problem that will surface as soon as stale windows run out.",[189,827,829],{"id":828},"common-mistakes","Common Mistakes",[194,831,832,842,852,858],{},[197,833,834,837,838,841],{},[200,835,836],{},"Caching responses that set cookies."," nginx refuses by default; overriding that with ",[30,839,840],{},"proxy_ignore_headers Set-Cookie"," can leak sessions.",[197,843,844,851],{},[200,845,846,847,850],{},"Missing ",[30,848,849],{},"always"," on add_header."," Without it, status headers are not added to some responses.",[197,853,854,857],{},[200,855,856],{},"Too small a keys_zone."," Each key consumes memory; a full zone evicts entries early.",[197,859,860,863],{},[200,861,862],{},"Cache keys without host."," Multi-site servers can serve one site's pages for another.",[189,865,867],{"id":866},"edge-cases","Edge Cases",[15,869,870,873,874,877,878,881],{},[200,871,872],{},"Vary."," nginx caches variants based on ",[30,875,876],{},"Vary"," headers from the upstream; ",[30,879,880],{},"Vary: Cookie"," effectively disables sharing.",[15,883,884,887,888,891],{},[200,885,886],{},"Large responses."," Buffering and temp paths matter for big files; ",[30,889,890],{},"use_temp_path=off"," avoids extra copies.",[15,893,894,897,898,900],{},[200,895,896],{},"Microcaching."," Caching for one second with ",[30,899,806],{}," is a classic trick that absorbs bursts for highly dynamic pages while keeping content nearly live.",[15,902,903,906],{},[200,904,905],{},"Behind a CDN."," nginx then acts as a shield; align its TTLs with the CDN's so both do not revalidate at the same moments.",[189,908,910],{"id":909},"faq","FAQ",[912,913,916,920],"details",{"className":914},[915],"faq-item",[917,918,919],"summary",{},"Does proxy_cache_background_update need use_stale updating?",[15,921,922,923,925],{},"Yes — background updates only happen when nginx is allowed to serve the stale entry while updating, which ",[30,924,36],{}," enables.",[912,927,929,932],{"className":928},[915],[917,930,931],{},"Is microcaching safe for dynamic pages?",[15,933,934],{},"For public pages, a one-second cache rarely shows meaningfully stale content and collapses bursts dramatically. Never apply it to personal responses.",[912,936,938,941],{"className":937},[915],[917,939,940],{},"Where should the cache live on disk?",[15,942,943],{},"On fast local storage (SSD or tmpfs for small caches). Network filesystems add latency and locking issues.",[912,945,947,950],{"className":946},[915],[917,948,949],{},"Can nginx revalidate with ETags?",[15,951,952,953,956,957,267,960,963],{},"With ",[30,954,955],{},"proxy_cache_revalidate on",", nginx uses ",[30,958,959],{},"If-Modified-Since",[30,961,962],{},"If-None-Match"," when refreshing expired entries, so unchanged content returns a cheap 304 from the app.",[912,965,967,970],{"className":966},[915],[917,968,969],{},"How does this interact with a CDN's SWR?",[15,971,972],{},"They layer: the CDN serves stale from the edge while revalidating against nginx, and nginx serves stale while revalidating against the app. Each layer reduces load on the next.",[912,974,976,979],{"className":975},[915],[917,977,978],{},"What status codes should be cached?",[15,980,981,982,45],{},"200 and 301 for normal validity, 404 briefly, never 5xx as fresh content — but allow serving stale on 5xx via ",[30,983,984],{},"proxy_cache_use_stale",[189,986,988],{"id":987},"related","Related",[194,990,991,998,1005],{},[197,992,993,997],{},[19,994,996],{"href":995},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fchoosing-swr-windows-for-html-and-apis\u002F","Choosing SWR windows for HTML and APIs"," — choosing the values nginx enforces.",[197,999,1000,1004],{},[19,1001,1003],{"href":1002},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Ftiered-caching-and-origin-shield\u002F","Tiered caching and origin shield"," — the CDN-side equivalent of this shield.",[197,1006,1007,1011],{},[19,1008,1010],{"href":1009},"\u002Fnetwork-protocol-optimization\u002Ftime-to-first-byte-optimization\u002Ffixing-ttfb-from-serverless-cold-starts\u002F","Fixing TTFB from serverless cold starts"," — another origin latency problem caching hides.",[1013,1014,1016],"script",{"type":1015},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Implement Stale-While-Revalidate with nginx proxy_cache\",\n  \"description\": \"Use nginx as a caching reverse proxy with background updates, cache locking and stale-on-error to cut origin load and TTFB.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Define a cache zone and enable caching for public routes\",\n      \"text\": \"server { location \u002F { proxypass http:\u002F\u002Fapp; proxycache html; proxycachekey \\\"$scheme$host$uri$isargs$args\\\"; proxycachevalid 200 301 10m; proxycachevalid 404 1m; addheader X-Cache-Status $upstreamcachestatus always; # trade-off: proxycachevalid only applies when the upstream sends no # Cache-Control\u002FE\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Serve stale while updating, in the background\",\n      \"text\": \"Expected outcome: expired entries are served instantly while one request refreshes them; concurrent misses wait for a single upstream fetch.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Honour stale-while-revalidate from the upstream\",\n      \"text\": \"nginx understands stale-while-revalidate and stale-if-error extensions in upstream Cache-Control headers (since 1.11.10), so the application can set windows per response while nginx enforces them.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Bypass the cache for personal requests\",\n      \"text\": \"Bypass the cache for personal requests\"\n    }\n  ]\n}\n",[1013,1018,1019],{"type":1015},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Implement Stale-While-Revalidate with nginx proxy_cache\",\n  \"description\": \"Use nginx as a caching reverse proxy with background updates, cache locking and stale-on-error to cut origin load and TTFB.\",\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\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-with-nginx-proxy-cache\u002F\"\n  }\n}\n",[1013,1021,1022],{"type":1015},"\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\": \"Stale-While-Revalidate Implementation\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Stale-While-Revalidate with nginx proxy_cache\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-with-nginx-proxy-cache\u002F\"\n    }\n  ]\n}\n",[1024,1025,1026],"style",{},"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 .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}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":351,"searchDepth":368,"depth":368,"links":1028},[1029,1030,1031,1037,1038,1039,1040,1041,1042,1043,1044],{"id":191,"depth":368,"text":192},{"id":234,"depth":368,"text":235},{"id":339,"depth":368,"text":340,"children":1032},[1033,1034,1035,1036],{"id":344,"depth":375,"text":345},{"id":526,"depth":375,"text":527},{"id":593,"depth":375,"text":594},{"id":612,"depth":375,"text":613},{"id":785,"depth":368,"text":786},{"id":796,"depth":368,"text":797},{"id":810,"depth":368,"text":811},{"id":817,"depth":368,"text":818},{"id":828,"depth":368,"text":829},{"id":866,"depth":368,"text":867},{"id":909,"depth":368,"text":910},{"id":987,"depth":368,"text":988},"Use nginx as a caching reverse proxy with background updates, cache locking and stale-on-error to cut origin load and TTFB.","md",{"slug":1048,"type":1049,"breadcrumb":1050,"datePublished":1057,"dateModified":1057},"stale-while-revalidate-with-nginx-proxy-cache","article",[1051,1053,1054,1055],{"name":1052,"url":267},"Home",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1056},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-with-nginx-proxy-cache\u002F","2026-10-06","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-with-nginx-proxy-cache",{"title":5,"description":1060},"Configure nginx proxy_cache to serve stale content while updating in the background, lock concurrent misses and survive upstream failures — an origin-side SWR layer.","advanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-with-nginx-proxy-cache\u002Findex","PjyZLQzvKtPgspI8wCtqPs5slgPfKPND89eqbtJRKuA",[1064,1068],{"title":1065,"path":1066,"stem":1067},"Stale-While-Revalidate Data Fetching with SWR and TanStack Query","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-data-fetching-with-swr-and-tanstack-query","advanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fstale-while-revalidate-data-fetching-with-swr-and-tanstack-query\u002Findex",{"title":1069,"path":1070,"stem":1071},"SWR via Cache-Control vs Service Worker revalidation","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation","advanced-caching-strategies-cdn-architecture\u002Fstale-while-revalidate-implementation\u002Fswr-cache-control-vs-service-worker-revalidation\u002Findex",1791308076679]