[{"data":1,"prerenderedAt":1174},["ShallowReactive",2],{"content:\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fstreaming-html-through-cdns-and-proxies":3,"surroundings:\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fstreaming-html-through-cdns-and-proxies":1165},{"id":4,"title":5,"body":6,"description":1145,"extension":1146,"meta":1147,"navigation":1158,"path":1159,"seo":1160,"stem":1163,"__hash__":1164},"content\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fstreaming-html-through-cdns-and-proxies\u002Findex.md","Streaming HTML Through CDNs and Proxies",{"type":7,"value":8,"toc":1127},"minimark",[9,13,28,31,131,136,174,178,184,190,196,202,206,211,395,399,484,491,495,506,510,517,617,621,624,628,635,793,797,808,906,910,913,917,943,947,953,959,965,971,975,987,1003,1012,1021,1030,1039,1048,1068,1077,1086,1090,1112,1117,1120,1123],[10,11,5],"h1",{"id":12},"streaming-html-through-cdns-and-proxies",[14,15,16,17,22,23,27],"p",{},"This guide is part of ",[18,19,21],"a",{"href":20},"\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002F","Streaming SSR & Early Flush",", within ",[18,24,26],{"href":25},"\u002Fnetwork-protocol-optimization\u002F","Network & Server Response Optimization",". Streaming only helps if the chunks reach the browser as they are written. Between your application and the user there are often several layers that can buffer: compression middleware in the app, a reverse proxy (nginx, Apache, Envoy), a load balancer or ingress controller, a serverless platform's adapter, and the CDN. Any one of them can collect the entire response before forwarding it. The application streams perfectly in local testing and buffers completely in production, and nobody notices because the page still works — it is just slower than it should be.",[14,29,30],{},"Fixing streaming in production means testing at each hop, finding the layer that buffers, and changing its configuration for HTML routes.",[14,32,33],{},[34,35,41,42,41,49,41,53,41,56,41,65,41,71,41,78,41,85,41,90,41,93,41,97,41,100,41,104,41,108,41,111,41,114,41,118,41,121,41,124,41,128,41],"svg",{"viewBox":36,"width":37,"role":38,"ariaLabel":39,"style":40},"0 0 760 318","100%","img","Layers between the application and the browser that commonly buffer streamed responses.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[43,44],"rect",{"className":45,"x":47,"y":47,"width":37,"height":37,"fill":48},[46],"svg-canvas","0","#ffffff",[50,51,52],"title",{},"Where responses get buffered",[54,55,39],"desc",{},[43,57],{"x":58,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"1","758","316","10","none","currentColor","stroke-opacity:0.18",[66,67,52],"text",{"x":68,"y":69,"fill":63,"style":70},"28.0","34.0","font-size:16px;font-weight:700",[43,72],{"x":68,"y":73,"width":74,"height":75,"rx":76,"fill":62,"stroke":63,"style":77},"56.0","704.0","40.0","6","stroke-opacity:0.35",[66,79,84],{"x":80,"y":81,"fill":63,"style":82,"textAnchor":83},"42.0","80.5","font-size:13px;font-weight:700","start","App compression middleware",[66,86,89],{"x":87,"y":81,"fill":63,"style":88,"textAnchor":83},"245.4","font-size:12px","Buffers to compress efficiently unless flushed",[43,91],{"x":68,"y":92,"width":74,"height":75,"rx":76,"fill":62,"stroke":63,"style":77},"106.0",[66,94,96],{"x":80,"y":95,"fill":63,"style":82,"textAnchor":83},"130.5","Reverse proxy (nginx, Apache)",[66,98,99],{"x":87,"y":95,"fill":63,"style":88,"textAnchor":83},"proxy_buffering \u002F fastcgi_buffering on by default",[43,101],{"x":68,"y":102,"width":74,"height":75,"rx":76,"fill":63,"stroke":63,"style":103},"156.0","fill-opacity:0.06;stroke-opacity:0.4",[66,105,107],{"x":80,"y":106,"fill":63,"style":82,"textAnchor":83},"180.5","Load balancer \u002F ingress",[66,109,110],{"x":87,"y":106,"fill":63,"style":88,"textAnchor":83},"Some modes buffer whole responses",[43,112],{"x":68,"y":113,"width":74,"height":75,"rx":76,"fill":62,"stroke":63,"style":77},"206.0",[66,115,117],{"x":80,"y":116,"fill":63,"style":82,"textAnchor":83},"230.5","Serverless adapter",[66,119,120],{"x":87,"y":116,"fill":63,"style":88,"textAnchor":83},"May only support buffered responses",[43,122],{"x":68,"y":123,"width":74,"height":75,"rx":76,"fill":63,"stroke":63,"style":103},"256.0",[66,125,127],{"x":80,"y":126,"fill":63,"style":82,"textAnchor":83},"280.5","CDN",[66,129,130],{"x":87,"y":126,"fill":63,"style":88,"textAnchor":83},"Usually streams; some features force buffering",[132,133,135],"h2",{"id":134},"rapid-diagnosis","Rapid Diagnosis",[137,138,139,152,158,168],"ul",{},[140,141,142,146,147,151],"li",{},[143,144,145],"strong",{},"Test at each hop"," with ",[148,149,150],"code",{},"curl -sN"," and timestamps: app directly, through the proxy, through the load balancer, through the CDN.",[140,153,154,157],{},[143,155,156],{},"Compare TTFB and download time"," in DevTools: streaming shows early TTFB and a long download; buffering shows late TTFB and a short download.",[140,159,160,163,164,167],{},[143,161,162],{},"Check compression",": does buffering appear only with ",[148,165,166],{},"Accept-Encoding: br","?",[140,169,170,173],{},[143,171,172],{},"Check platform documentation"," for streaming support in your hosting environment.",[132,175,177],{"id":176},"root-cause-analysis","Root Cause Analysis",[14,179,180,183],{},[143,181,182],{},"1. Proxy buffering defaults."," nginx buffers proxied and FastCGI responses by default.",[14,185,186,189],{},[143,187,188],{},"2. Compression buffering."," Compressors wait for enough data unless flushed.",[14,191,192,195],{},[143,193,194],{},"3. Platform limitations."," Some serverless runtimes return responses only when complete.",[14,197,198,201],{},[143,199,200],{},"4. CDN features."," HTML rewriting, edge-side includes or certain security features may buffer responses.",[132,203,205],{"id":204},"step-by-step-resolution","Step-by-Step Resolution",[207,208,210],"h3",{"id":209},"_1-locate-the-buffering-hop","1. Locate the buffering hop",[212,213,218],"pre",{"className":214,"code":215,"language":216,"meta":217,"style":217},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# Timestamped chunks at each layer (adjust hosts and ports).\nt() { curl -sN --compressed \"$1\" | while IFS= read -r l; do printf '%s %s\\n' \"$(date +%T.%3N)\" \"${l:0:50}\"; done | head -5; }\nt http:\u002F\u002F127.0.0.1:3000\u002Fproduct\u002F42           # app\nt http:\u002F\u002F127.0.0.1:8080\u002Fproduct\u002F42           # via nginx\nt https:\u002F\u002Forigin.example.com\u002Fproduct\u002F42      # via load balancer\nt https:\u002F\u002Fwww.example.com\u002Fproduct\u002F42         # via CDN\n# trade-off: the first lines appearing at the same time as the last means that\n# hop buffered; compare across hops to find the first one that does.\n","bash","",[148,219,220,229,339,350,361,372,383,389],{"__ignoreMap":217},[221,222,225],"span",{"class":223,"line":224},"line",1,[221,226,228],{"class":227},"sjfSM","# Timestamped chunks at each layer (adjust hosts and ports).\n",[221,230,232,236,240,244,248,251,255,258,261,265,268,271,274,277,280,283,286,289,292,295,298,301,304,307,310,313,315,317,320,323,325,328,330,333,336],{"class":223,"line":231},2,[221,233,235],{"class":234},"smZ65","t",[221,237,239],{"class":238},"saISM","() { ",[221,241,243],{"class":242},"sQw3B","curl",[221,245,247],{"class":246},"sPXB4"," -sN",[221,249,250],{"class":246}," --compressed",[221,252,254],{"class":253},"sZ8jY"," \"",[221,256,257],{"class":246},"$1",[221,259,260],{"class":253},"\"",[221,262,264],{"class":263},"sPARh"," |",[221,266,267],{"class":263}," while",[221,269,270],{"class":238}," IFS",[221,272,273],{"class":263},"=",[221,275,276],{"class":246}," read",[221,278,279],{"class":246}," -r",[221,281,282],{"class":253}," l",[221,284,285],{"class":238},"; ",[221,287,288],{"class":263},"do",[221,290,291],{"class":246}," printf",[221,293,294],{"class":253}," '%s %s\\n'",[221,296,297],{"class":253}," \"$(",[221,299,300],{"class":242},"date",[221,302,303],{"class":253}," +%T.%3N)\"",[221,305,306],{"class":253}," \"${",[221,308,309],{"class":238},"l",[221,311,312],{"class":263},":",[221,314,47],{"class":238},[221,316,312],{"class":263},[221,318,319],{"class":238},"50",[221,321,322],{"class":253},"}\"",[221,324,285],{"class":238},[221,326,327],{"class":263},"done",[221,329,264],{"class":263},[221,331,332],{"class":242}," head",[221,334,335],{"class":246}," -5",[221,337,338],{"class":238},"; }\n",[221,340,342,344,347],{"class":223,"line":341},3,[221,343,235],{"class":242},[221,345,346],{"class":253}," http:\u002F\u002F127.0.0.1:3000\u002Fproduct\u002F42",[221,348,349],{"class":227},"           # app\n",[221,351,353,355,358],{"class":223,"line":352},4,[221,354,235],{"class":242},[221,356,357],{"class":253}," http:\u002F\u002F127.0.0.1:8080\u002Fproduct\u002F42",[221,359,360],{"class":227},"           # via nginx\n",[221,362,364,366,369],{"class":223,"line":363},5,[221,365,235],{"class":242},[221,367,368],{"class":253}," https:\u002F\u002Forigin.example.com\u002Fproduct\u002F42",[221,370,371],{"class":227},"      # via load balancer\n",[221,373,375,377,380],{"class":223,"line":374},6,[221,376,235],{"class":242},[221,378,379],{"class":253}," https:\u002F\u002Fwww.example.com\u002Fproduct\u002F42",[221,381,382],{"class":227},"         # via CDN\n",[221,384,386],{"class":223,"line":385},7,[221,387,388],{"class":227},"# trade-off: the first lines appearing at the same time as the last means that\n",[221,390,392],{"class":223,"line":391},8,[221,393,394],{"class":227},"# hop buffered; compare across hops to find the first one that does.\n",[207,396,398],{"id":397},"_2-disable-proxy-buffering-for-html","2. Disable proxy buffering for HTML",[212,400,404],{"className":401,"code":402,"language":403,"meta":217,"style":217},"language-nginx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","location \u002F {\n  proxy_pass http:\u002F\u002Fapp;\n  proxy_buffering off;            # pass chunks through as they arrive\n  proxy_http_version 1.1;\n  gzip off;                       # let the app or CDN compress, or ensure flushing\n}\n# For FastCGI (PHP): fastcgi_buffering off;\n# trade-off: disabling buffering ties up a proxy connection for the full response\n# duration; acceptable for HTML, keep buffering on for slow clients on large files.\n","nginx",[148,405,406,417,425,439,450,463,468,473,478],{"__ignoreMap":217},[221,407,408,411,414],{"class":223,"line":224},[221,409,410],{"class":263},"location",[221,412,413],{"class":242}," \u002F ",[221,415,416],{"class":238},"{\n",[221,418,419,422],{"class":223,"line":231},[221,420,421],{"class":263},"  proxy_pass ",[221,423,424],{"class":238},"http:\u002F\u002Fapp;\n",[221,426,427,430,433,436],{"class":223,"line":341},[221,428,429],{"class":263},"  proxy_buffering ",[221,431,432],{"class":246},"off",[221,434,435],{"class":238},";            ",[221,437,438],{"class":227},"# pass chunks through as they arrive\n",[221,440,441,444,447],{"class":223,"line":352},[221,442,443],{"class":263},"  proxy_http_version ",[221,445,446],{"class":246},"1.1",[221,448,449],{"class":238},";\n",[221,451,452,455,457,460],{"class":223,"line":363},[221,453,454],{"class":263},"  gzip ",[221,456,432],{"class":246},[221,458,459],{"class":238},";                       ",[221,461,462],{"class":227},"# let the app or CDN compress, or ensure flushing\n",[221,464,465],{"class":223,"line":374},[221,466,467],{"class":238},"}\n",[221,469,470],{"class":223,"line":385},[221,471,472],{"class":227},"# For FastCGI (PHP): fastcgi_buffering off;\n",[221,474,475],{"class":223,"line":391},[221,476,477],{"class":227},"# trade-off: disabling buffering ties up a proxy connection for the full response\n",[221,479,481],{"class":223,"line":480},9,[221,482,483],{"class":227},"# duration; acceptable for HTML, keep buffering on for slow clients on large files.\n",[14,485,486,487,490],{},"Applications can also send ",[148,488,489],{},"X-Accel-Buffering: no"," to disable nginx buffering per response.",[207,492,494],{"id":493},"_3-flush-compression-per-chunk","3. Flush compression per chunk",[14,496,497,498,501,502,505],{},"In Node.js with the ",[148,499,500],{},"compression"," middleware, call ",[148,503,504],{},"res.flush()"," after each chunk (frameworks with built-in streaming often handle this). For Brotli and gzip streams, use flush modes that emit compressed data immediately.",[207,507,509],{"id":508},"_4-use-streaming-capable-platform-apis-and-cdn-settings","4. Use streaming-capable platform APIs and CDN settings",[14,511,512,513,516],{},"Use the platform's streaming response APIs (Web ",[148,514,515],{},"ReadableStream"," responses on edge platforms, streaming-enabled function configurations). Check CDN features such as HTML rewriting or bot protection challenges that may force buffering, and disable them for streamed routes if necessary.",[14,518,519],{},[34,520,41,523,41,526,41,529,41,531,41,534,41,536,41,541,41,550,41,557,41,560,41,564,41,569,41,572,41,576,41,581,41,587,41,593,41,597,41,601,41,605,41,609,41,613,41],{"viewBox":521,"width":37,"role":38,"ariaLabel":522,"style":40},"0 0 760 216","Timeline showing when the first chunk of a streamed response reaches each layer, revealing the buffering hop.",[43,524],{"className":525,"x":47,"y":47,"width":37,"height":37,"fill":48},[46],[50,527,528],{},"First chunk arrival at each hop (before fixes)",[54,530,522],{},[43,532],{"x":58,"y":58,"width":59,"height":533,"rx":61,"fill":62,"stroke":63,"style":64},"214",[66,535,528],{"x":68,"y":69,"fill":63,"style":70},[66,537,540],{"x":68,"y":538,"fill":63,"style":539},"74.0","font-size:12.5px;font-weight:700","App",[43,542],{"x":543,"y":73,"width":544,"height":545,"rx":546,"fill":547,"stroke":548,"style":549},"75.2","31.1","26.0","3","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[66,551,556],{"x":552,"y":553,"fill":63,"style":554,"textAnchor":555},"90.7","73.5","font-size:11.5px","middle","head",[66,558,403],{"x":68,"y":559,"fill":63,"style":539},"112.0",[43,561],{"x":543,"y":562,"width":563,"height":545,"rx":546,"fill":62,"stroke":63,"style":77},"94.0","541.5",[66,565,568],{"x":566,"y":567,"fill":63,"style":554,"textAnchor":555},"345.9","111.5","buffered whole response",[66,570,127],{"x":68,"y":571,"fill":63,"style":539},"150.0",[43,573],{"x":543,"y":574,"width":575,"height":545,"rx":546,"fill":62,"stroke":63,"style":77},"132.0","563.2",[66,577,580],{"x":578,"y":579,"fill":63,"style":554,"textAnchor":555},"356.8","149.5","waits for nginx",[223,582],{"x1":583,"y1":584,"x2":585,"y2":584,"stroke":63,"style":586},"74.5","172.0","726.0","stroke-opacity:0.4",[66,588,592],{"x":589,"y":590,"fill":591,"style":554,"textAnchor":555},"85.1","189.0","#51617a","0ms",[66,594,596],{"x":595,"y":590,"fill":591,"style":554,"textAnchor":555},"183.0","200ms",[66,598,600],{"x":599,"y":590,"fill":591,"style":554,"textAnchor":555},"291.6","400ms",[66,602,604],{"x":603,"y":590,"fill":591,"style":554,"textAnchor":555},"400.2","600ms",[66,606,608],{"x":607,"y":590,"fill":591,"style":554,"textAnchor":555},"508.8","800ms",[66,610,612],{"x":611,"y":590,"fill":591,"style":554,"textAnchor":555},"617.4","1000ms",[66,614,616],{"x":615,"y":590,"fill":591,"style":554,"textAnchor":555},"712.3","1200ms",[132,618,620],{"id":619},"verification","Verification",[14,622,623],{},"Run the timestamped curl test through the full production path: the head should arrive within milliseconds of the app writing it. In the browser, the document request's \"Waiting for server response\" should be short and \"Content Download\" longer. LCP for uncached pages should improve correspondingly.",[132,625,627],{"id":626},"worked-example-a-saas-marketing-site-on-kubernetes","Worked Example: A SaaS Marketing Site on Kubernetes",[14,629,630,631,634],{},"A SaaS company implemented streaming SSR in its Next.js marketing site. Local tests showed the head arriving in 40ms, but production TTFB remained at 650ms. Hop-by-hop testing found two buffering layers: the nginx ingress controller (default ",[148,632,633],{},"proxy-buffering"," on) and a custom compression middleware in the Node server that buffered until the response ended. Setting the ingress annotation to disable buffering for the HTML service and removing the custom middleware (letting the CDN compress) brought production TTFB to 80ms. LCP p75 for uncached pages improved by 520ms.",[14,636,637],{},[34,638,41,641,41,644,41,647,41,649,41,666,41,669,41,671,41,675,41,680,41,683,41,688,41,695,41,702,41,706,41,711,41,714,41,718,41,721,41,724,41,727,41,730,41,733,41,736,41,739,41,743,41,745,41,748,41,751,41,754,41,758,41,761,41,764,41,768,41,770,41,773,41,776,41,779,41,783,41,786,41,789,41],{"viewBox":639,"width":37,"role":38,"ariaLabel":640,"style":40},"0 0 760 396","Decision sequence for removing buffering at the layer that holds the stream.",[43,642],{"className":643,"x":47,"y":47,"width":37,"height":37,"fill":48},[46],[50,645,646],{},"Fixing a buffering hop",[54,648,640],{},[650,651,652],"defs",{},[653,654,661],"marker",{"id":655,"viewBox":656,"refX":657,"refY":658,"markerWidth":659,"markerHeight":659,"orient":660},"fa6e351315","0 0 10 10","9","5","7","auto-start-reverse",[662,663],"path",{"d":664,"fill":63,"style":665},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[43,667],{"x":58,"y":58,"width":59,"height":668,"rx":61,"fill":62,"stroke":63,"style":64},"394",[66,670,646],{"x":68,"y":69,"fill":63,"style":70},[43,672],{"x":68,"y":73,"width":673,"height":674,"rx":76,"fill":63,"stroke":63,"style":103},"320.0","35.0",[66,676,679],{"x":677,"y":678,"fill":63,"style":82,"textAnchor":555},"188.0","78.0","Does the app itself send chunks?",[43,681],{"x":682,"y":73,"width":673,"height":674,"rx":76,"fill":62,"stroke":63,"style":77},"412.0",[66,684,687],{"x":685,"y":678,"fill":63,"style":686,"textAnchor":555},"572.0","font-size:12.5px","Fix the app first (streaming renderer, flush)",[223,689],{"x1":690,"y1":553,"x2":691,"y2":553,"stroke":63,"strokeWidth":692,"style":693,"markerEnd":694},"348.0","410.0","1.5","stroke-opacity:0.6","url(#fa6e351315)",[66,696,701],{"x":697,"y":698,"fill":699,"style":700,"textAnchor":555},"380.0","66.5","#0466c8","font-size:11.5px;font-weight:700","yes",[223,703],{"x1":677,"y1":704,"x2":677,"y2":705,"stroke":63,"strokeWidth":692,"style":693,"markerEnd":694},"91.0","125.0",[66,707,710],{"x":708,"y":709,"fill":591,"style":700},"196.0","113.0","no",[43,712],{"x":68,"y":713,"width":673,"height":674,"rx":76,"fill":63,"stroke":63,"style":103},"127.0",[66,715,717],{"x":677,"y":716,"fill":63,"style":82,"textAnchor":555},"149.0","Does nginx or the ingress buffer?",[43,719],{"x":682,"y":713,"width":673,"height":674,"rx":76,"fill":699,"stroke":699,"style":720},"fill-opacity:0.14;stroke-opacity:0.9",[66,722,723],{"x":685,"y":716,"fill":63,"style":686,"textAnchor":555},"proxy_buffering off or X-Accel-Buffering no",[223,725],{"x1":690,"y1":726,"x2":691,"y2":726,"stroke":63,"strokeWidth":692,"style":693,"markerEnd":694},"144.5",[66,728,701],{"x":697,"y":729,"fill":699,"style":700,"textAnchor":555},"137.5",[223,731],{"x1":677,"y1":732,"x2":677,"y2":708,"stroke":63,"strokeWidth":692,"style":693,"markerEnd":694},"162.0",[66,734,710],{"x":708,"y":735,"fill":591,"style":700},"184.0",[43,737],{"x":68,"y":738,"width":673,"height":674,"rx":76,"fill":63,"stroke":63,"style":103},"198.0",[66,740,742],{"x":677,"y":741,"fill":63,"style":82,"textAnchor":555},"220.0","Does buffering appear only when compressed?",[43,744],{"x":682,"y":738,"width":673,"height":674,"rx":76,"fill":699,"stroke":699,"style":720},[66,746,747],{"x":685,"y":741,"fill":63,"style":686,"textAnchor":555},"Flush the compressor or compress at the CDN",[223,749],{"x1":690,"y1":750,"x2":691,"y2":750,"stroke":63,"strokeWidth":692,"style":693,"markerEnd":694},"215.5",[66,752,701],{"x":697,"y":753,"fill":699,"style":700,"textAnchor":555},"208.5",[223,755],{"x1":677,"y1":756,"x2":677,"y2":757,"stroke":63,"strokeWidth":692,"style":693,"markerEnd":694},"233.0","267.0",[66,759,710],{"x":708,"y":760,"fill":591,"style":700},"255.0",[43,762],{"x":68,"y":763,"width":673,"height":674,"rx":76,"fill":63,"stroke":63,"style":103},"269.0",[66,765,767],{"x":677,"y":766,"fill":63,"style":82,"textAnchor":555},"291.0","Does the CDN buffer?",[43,769],{"x":682,"y":763,"width":673,"height":674,"rx":76,"fill":699,"stroke":699,"style":720},[66,771,772],{"x":685,"y":766,"fill":63,"style":686,"textAnchor":555},"Disable buffering features for HTML routes",[223,774],{"x1":690,"y1":775,"x2":691,"y2":775,"stroke":63,"strokeWidth":692,"style":693,"markerEnd":694},"286.5",[66,777,701],{"x":697,"y":778,"fill":699,"style":700,"textAnchor":555},"279.5",[223,780],{"x1":677,"y1":781,"x2":677,"y2":782,"stroke":63,"strokeWidth":692,"style":693,"markerEnd":694},"304.0","338.0",[66,784,710],{"x":708,"y":785,"fill":591,"style":700},"326.0",[43,787],{"x":68,"y":788,"width":673,"height":69,"rx":76,"fill":547,"stroke":548,"style":549},"340.0",[66,790,792],{"x":677,"y":791,"fill":63,"style":686,"textAnchor":555},"361.5","Check the serverless adapter's streaming mode",[132,794,796],{"id":795},"monitoring-for-regressions","Monitoring for Regressions",[14,798,799,800,803,804,807],{},"Buffering often returns silently: an infrastructure upgrade resets a default, a new CDN feature is enabled, or a middleware is added. Add a synthetic check that requests a streamed page through the production path and measures the gap between the first and last byte; if the gap collapses to nearly zero while TTFB grows, something started buffering. In RUM, track ",[148,801,802],{},"responseStart"," and ",[148,805,806],{},"responseEnd"," of the navigation entry: for streamed pages, the difference should remain substantial.",[212,809,813],{"className":810,"code":811,"language":812,"meta":217,"style":217},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","const [nav] = performance.getEntriesByType('navigation');\nconst streamGap = nav.responseEnd - nav.responseStart;   \u002F\u002F ~0 when buffered\nnavigator.sendBeacon('\u002Frum', JSON.stringify({ ttfb: nav.responseStart, streamGap }));\n\u002F\u002F trade-off: cached pages also have a small gap; compare uncached routes only, or\n\u002F\u002F segment by a cache-status Server-Timing entry.\n","javascript",[148,814,815,846,868,896,901],{"__ignoreMap":217},[221,816,817,820,823,826,829,831,834,837,840,843],{"class":223,"line":224},[221,818,819],{"class":263},"const",[221,821,822],{"class":238}," [",[221,824,825],{"class":246},"nav",[221,827,828],{"class":238},"] ",[221,830,273],{"class":263},[221,832,833],{"class":238}," performance.",[221,835,836],{"class":234},"getEntriesByType",[221,838,839],{"class":238},"(",[221,841,842],{"class":253},"'navigation'",[221,844,845],{"class":238},");\n",[221,847,848,850,853,856,859,862,865],{"class":223,"line":231},[221,849,819],{"class":263},[221,851,852],{"class":246}," streamGap",[221,854,855],{"class":263}," =",[221,857,858],{"class":238}," nav.responseEnd ",[221,860,861],{"class":263},"-",[221,863,864],{"class":238}," nav.responseStart;   ",[221,866,867],{"class":227},"\u002F\u002F ~0 when buffered\n",[221,869,870,873,876,878,881,884,887,890,893],{"class":223,"line":341},[221,871,872],{"class":238},"navigator.",[221,874,875],{"class":234},"sendBeacon",[221,877,839],{"class":238},[221,879,880],{"class":253},"'\u002Frum'",[221,882,883],{"class":238},", ",[221,885,886],{"class":246},"JSON",[221,888,889],{"class":238},".",[221,891,892],{"class":234},"stringify",[221,894,895],{"class":238},"({ ttfb: nav.responseStart, streamGap }));\n",[221,897,898],{"class":223,"line":352},[221,899,900],{"class":227},"\u002F\u002F trade-off: cached pages also have a small gap; compare uncached routes only, or\n",[221,902,903],{"class":223,"line":363},[221,904,905],{"class":227},"\u002F\u002F segment by a cache-status Server-Timing entry.\n",[132,907,909],{"id":908},"load-balancers-and-service-meshes","Load Balancers and Service Meshes",[14,911,912],{},"Cloud load balancers and service meshes sit between the proxy and the application in many deployments. Most HTTP load balancers stream responses, but some modes or features (response inspection, certain health-check integrations, request mirroring) buffer. Service mesh sidecars like Envoy stream by default but can be configured with buffering filters. When the hop-by-hop test shows buffering between the ingress and the app, check these layers' configuration and documentation for response buffering options.",[132,914,916],{"id":915},"common-mistakes","Common Mistakes",[137,918,919,925,931,937],{},[140,920,921,924],{},[143,922,923],{},"Testing only locally."," Production paths have more hops.",[140,926,927,930],{},[143,928,929],{},"Disabling buffering everywhere."," Only HTML routes that stream need it.",[140,932,933,936],{},[143,934,935],{},"Ignoring compression."," Compressed responses buffer while uncompressed tests stream.",[140,938,939,942],{},[143,940,941],{},"Assuming the CDN streams."," Check its features and documentation.",[132,944,946],{"id":945},"edge-cases","Edge Cases",[14,948,949,952],{},[143,950,951],{},"HTTP\u002F1.0 clients."," Without chunked encoding, responses are sent with connection close; rare in practice.",[14,954,955,958],{},[143,956,957],{},"WAFs and bot protection."," Some inspect full responses; configure exceptions or test their effect.",[14,960,961,964],{},[143,962,963],{},"Server-Sent Events and streaming APIs."," The same buffering issues apply and the same fixes help.",[14,966,967,970],{},[143,968,969],{},"Edge HTML rewriting."," Streaming HTML rewriters exist (process chunks as they pass); non-streaming rewriters force buffering.",[132,972,974],{"id":973},"faq","FAQ",[976,977,980,984],"details",{"className":978},[979],"faq-item",[981,982,983],"summary",{},"Why does streaming work locally but not in production?",[14,985,986],{},"Usually a proxy, load balancer, compression layer or platform adapter in production buffers responses.",[976,988,990,993],{"className":989},[979],[981,991,992],{},"How do I disable nginx buffering for one route?",[14,994,995,996,999,1000,1002],{},"Set ",[148,997,998],{},"proxy_buffering off"," in that location block, or send ",[148,1001,489],{}," from the application.",[976,1004,1006,1009],{"className":1005},[979],[981,1007,1008],{},"Do CDNs support streaming?",[14,1010,1011],{},"Most major CDNs pass streamed responses through. Some features, like full-page HTML rewriting, may buffer. Check your provider's documentation.",[976,1013,1015,1018],{"className":1014},[979],[981,1016,1017],{},"Does compression prevent streaming?",[14,1019,1020],{},"Not if the compressor flushes each chunk. Buffering compressors do prevent it.",[976,1022,1024,1027],{"className":1023},[979],[981,1025,1026],{},"Is it safe to disable proxy buffering?",[14,1028,1029],{},"For HTML responses, generally yes. Buffering protects backends from slow clients; with a CDN in front, the CDN usually absorbs slow clients.",[976,1031,1033,1036],{"className":1032},[979],[981,1034,1035],{},"Does HTTP\u002F2 change anything?",[14,1037,1038],{},"HTTP\u002F2 and HTTP\u002F3 stream frames natively, but intermediaries can still buffer the full body before forwarding.",[976,1040,1042,1045],{"className":1041},[979],[981,1043,1044],{},"Do serverless platforms support streaming?",[14,1046,1047],{},"Many now offer streaming response modes or Web stream APIs; some older configurations buffer. Check the platform's streaming documentation.",[976,1049,1051,1054],{"className":1050},[979],[981,1052,1053],{},"Can I stream through Cloudflare Workers?",[14,1055,1056,1057,1060,1061,1063,1064,1067],{},"Yes. Returning a ",[148,1058,1059],{},"Response"," with a ",[148,1062,515],{}," body streams to the client, and ",[148,1065,1066],{},"HTMLRewriter"," processes HTML in a streaming fashion.",[976,1069,1071,1074],{"className":1070},[979],[981,1072,1073],{},"Does a buffering hop make streaming harmful?",[14,1075,1076],{},"No, just ineffective: the response arrives as if it had been rendered in full. Finding and removing the buffering hop restores the benefit.",[976,1078,1080,1083],{"className":1079},[979],[981,1081,1082],{},"Should HTTP\u002F2 be enabled between proxy and app?",[14,1084,1085],{},"It is not required for streaming. HTTP\u002F1.1 with chunked transfer encoding streams fine between internal hops, as long as buffering is disabled.",[132,1087,1089],{"id":1088},"related","Related",[137,1091,1092,1099,1106],{},[140,1093,1094,1098],{},[18,1095,1097],{"href":1096},"\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fflushing-the-document-head-early\u002F","Flushing the document head early"," — what to stream first.",[140,1100,1101,1105],{},[18,1102,1104],{"href":1103},"\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Fenabling-brotli-on-nginx-and-cdns\u002F","Enabling Brotli on nginx and CDNs"," — compression settings that interact with streaming.",[140,1107,1108,1111],{},[18,1109,1110],{"href":20},"Streaming SSR & early flush"," — the topic overview.",[1113,1114,1116],"script",{"type":1115},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Streaming HTML Through CDNs and Proxies\",\n  \"description\": \"How to find and remove buffering between your server and the browser so streamed and early-flushed HTML actually arrives progressively.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Locate the buffering hop\",\n      \"text\": \"Locate the buffering hop\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Disable proxy buffering for HTML\",\n      \"text\": \"Applications can also send X-Accel-Buffering: no to disable nginx buffering per response.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Flush compression per chunk\",\n      \"text\": \"In Node.js with the compression middleware, call res.flush() after each chunk (frameworks with built-in streaming often handle this).\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Use streaming-capable platform APIs and CDN settings\",\n      \"text\": \"Use the platform's streaming response APIs (Web ReadableStream responses on edge platforms, streaming-enabled function configurations).\"\n    }\n  ]\n}\n",[1113,1118,1119],{"type":1115},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"Streaming HTML Through CDNs and Proxies\",\n  \"description\": \"How to find and remove buffering between your server and the browser so streamed and early-flushed HTML actually arrives progressively.\",\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\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fstreaming-html-through-cdns-and-proxies\u002F\"\n  }\n}\n",[1113,1121,1122],{"type":1115},"\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\": \"Network & Server Response Optimization\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fnetwork-protocol-optimization\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Streaming SSR & Early Flush\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Streaming HTML Through CDNs and Proxies\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fstreaming-html-through-cdns-and-proxies\u002F\"\n    }\n  ]\n}\n",[1124,1125,1126],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}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 .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}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 .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":217,"searchDepth":231,"depth":231,"links":1128},[1129,1130,1131,1137,1138,1139,1140,1141,1142,1143,1144],{"id":134,"depth":231,"text":135},{"id":176,"depth":231,"text":177},{"id":204,"depth":231,"text":205,"children":1132},[1133,1134,1135,1136],{"id":209,"depth":341,"text":210},{"id":397,"depth":341,"text":398},{"id":493,"depth":341,"text":494},{"id":508,"depth":341,"text":509},{"id":619,"depth":231,"text":620},{"id":626,"depth":231,"text":627},{"id":795,"depth":231,"text":796},{"id":908,"depth":231,"text":909},{"id":915,"depth":231,"text":916},{"id":945,"depth":231,"text":946},{"id":973,"depth":231,"text":974},{"id":1088,"depth":231,"text":1089},"How to find and remove buffering between your server and the browser so streamed and early-flushed HTML actually arrives progressively.","md",{"slug":12,"type":1148,"breadcrumb":1149,"datePublished":1157,"dateModified":1157},"article",[1150,1153,1154,1155],{"name":1151,"url":1152},"Home","\u002F",{"name":26,"url":25},{"name":21,"url":20},{"name":5,"url":1156},"\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fstreaming-html-through-cdns-and-proxies\u002F","2026-10-06",true,"\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fstreaming-html-through-cdns-and-proxies",{"title":1161,"description":1162},"Streaming HTML Through CDNs, Proxies and Middleware","Streaming breaks when nginx, load balancers, compression middleware, serverless adapters or CDNs buffer responses. Find each buffering hop and turn it off.","network-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fstreaming-html-through-cdns-and-proxies\u002Findex","YN2h_IBspNll5VUWrctVDyrNWT1KKhHkRhv4j27Ykpo",[1166,1170],{"title":1167,"path":1168,"stem":1169},"Flushing the Document Head Early","\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fflushing-the-document-head-early","network-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fflushing-the-document-head-early\u002Findex",{"title":1171,"path":1172,"stem":1173},"Streaming SSR with React Suspense","\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fstreaming-ssr-with-react-suspense","network-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002Fstreaming-ssr-with-react-suspense\u002Findex",1791308077105]