A reverse proxy receives requests on behalf of a service and then initiates exchanges with the backend; load balancing decides which available target receives a connection or request. The two are often implemented in a single component, but they are not the same concept: you may need a reverse proxy even with only one backend, and a Layer 4 load balancer may not parse HTTP at all.
Taking https://api.example.com/orders as an example, a user request might first pass through an edge entry point, then to a regional proxy, and finally to an order instance. Each hop has its own connection, timeout, and identity boundaries; a 502 seen by the client might correspond to a backend request that was already submitted but failed during the response transmission.
Problem to Solve
Clarify First
Routing by Host or Path
Which layer can decrypt and parse HTTP
Backend sees wrong domain or protocol
Original Host, upstream TLS name, and forwarding metadata
Instance request counts are even, but load is not
Allocation unit and cost per request
Streaming stuck or SSE bursts
Buffering, flushing, and idle timeouts
Auto-retries exacerbate failures
Attempt budget, concurrency, and idempotency contracts
Old traffic persists after decommission
Connection draining, pooling, and long-connection state
Two Connections, Three Easily Confused Identities
Layer 7 proxies typically terminate the downstream TLS and establish an independent upstream connection. The client verifies the public service certificate provided by the proxy; if the proxy accesses the backend via HTTPS, it must also verify the backend's identity. The TLS client seen by the backend might be the proxy, not the end user.
You must also record separately: the authority of the client's original request, the TLS ServerName when the proxy connects to the upstream, and the external service name used by the application to generate absolute URLs. They may be the same or different. Changing the Host to 127.0.0.1:3000 everywhere will cause some applications to generate incorrect redirects; keeping the external Host might not meet the virtual host requirements of the hosted upstream.
A forward proxy represents the client choosing the target, while a reverse proxy represents the service providing the entry point; "hiding addresses" is a possible outcome, not the definition. A proxy may also actively pass the original address, so you cannot simply define them by who is anonymized.
L4 / L7: The Unit of Selection Matters More Than Speed Labels
Method
Usually Allocated By
Visible Information and Limitations
L4 Connection Forwarding
New TCP connections, etc.
Usually does not understand HTTP paths; existing connections are pinned to a target
TLS Passthrough and Reading Visible Handshake Info
Connections and applicable SNI, etc.
Does not decrypt HTTP; ECH etc. change visible information
L7 HTTP Proxy
Requests, streams, and implementation-specific pooling strategies
Can route by method, Host, path, or authentication results
An HTTP/2 connection can carry many requests. Assigning this connection to one backend via L4 does not mean each request within it is load-balanced separately; L7 can select the upstream per request, but is still constrained by connection pools, stream concurrency, and affinity policies.
Least connections is not exclusive to L7; for example, NGINX stream upstream supports least_conn. Conversely, few connections do not necessarily mean low business load: one connection may carry many HTTP/2 streams or one expensive long-running task. NGINX stream upstream
L4 not parsing HTTP usually reduces some processing overhead, but you cannot declare that all L4 deployments are faster than L7 without considering TLS, copying, cache hits, and hardware paths. WebSockets do not require L4 only; HTTP proxies that support handshake and tunnel forwarding can also carry them.
Balancing Algorithms: Ask First What Is Being Balanced—Count, Connections, or Cost
Algorithm
Applicable Understanding
Typical Bias
Round Robin / Weighted Round Robin
Scheduling selection count distributed sequentially or by weight
Request costs differ; CPU and bytes may not be proportional
Least Connections / Least Active Requests
Selects targets with lower observed load
Metric granularity, short/long tasks, and multiplexing affect effectiveness
Random / Two-Choice Random
Reduces centralized decision cost
Results are affected by sample size and local view
IP or Session Affinity
Keeps the same key on the same target when possible
NAT large client hotspots, address changes, and failover
Consistent Hashing / HRW
Reduces key migration when members change
Does not guarantee exactly 1/N migration each time, nor eliminate hotspots
For example, if A is assigned 100 requests of 2ms each, and B is assigned 100 requests of 200ms each, the scheduling count is completely uniform, but the serial work volume differs by 100 times. A weight of 3:1 only constrains the selection share under its specific selection model; it does not guarantee three times the bytes or three times the CPU usage.
If healthy instances have a combined average processing capacity of 1000 requests/second, but the entry point continuously receives 1200, changing the balancing algorithm cannot eliminate the continuously growing queue. You need queue limits, backpressure, rate limiting, or scaling, and must leave budget for retries. NGINX HTTP upstream
Client Address and Forwarding Headers: Trusted Source Before Field Values
Forwarded, X-Forwarded-For, X-Forwarded-Proto, etc., can express information observed by the previous hop, but external clients can also construct fields with the same names. The backend cannot unconditionally take the leftmost address for auditing, rate limiting, or access control.
You should define which directly connected sources are trusted proxies, as well as the rules for cleaning, appending, and overriding at each layer. A multi-layer chain can parse forward from the known trusted end, stopping at untrusted boundaries; private network segments do not naturally equate to controlled proxies. Forwarded itself does not provide cryptographic signatures. RFC 7239
The PROXY protocol is another mechanism for passing metadata at the beginning of a connection; it is not an HTTP header and does not automatically authenticate the sender; it must be enabled by both parties on a limited entry point. Sending it to a listener expecting standard TLS or HTTP will cause parsing failures.
The following configuration is provided for offline understanding: route to two local instances at the local port 8080 entry point by path, preserving the /api/ prefix. There is no public certificate application, nor does it rely on a real domain name.
handle does not strip the matching prefix; if changed to handle_path, you must confirm that the URI expected by the backend changes accordingly. /api/* does not automatically include the exact path /api; the entry contract should be explicit.
Caddy handles X-Forwarded-For/Proto/Host by default and ignores corresponding values passed from untrusted entries; what needs additional configuration is the trusted proxy boundary, not unconditionally overwriting all forwarding fields. This uses the HTTP upstream; the Host behavior for HTTPS upstreams varies by version. Starting from Caddy 2.11.0, it can automatically match the upstream Host; older versions should explicitly configure the actual name. Caddy reverse_proxy
This example has been adapted and checked offline with Caddy 2.10.2; passing the configuration syntax does not mean the backend /readyz implementation is correct, nor does it mean network traffic has been verified. Automatic HTTPS, TLS trust, and production exposure addresses should be configured separately based on deployment.
Timeouts and Retries: Constrain All Attempts with the Same Business Deadline
Connection timeout limits connection establishment, response header timeout limits waiting for headers, read idle timeout limits the interval between two progress points, and end-to-end deadline limits the total business time. They cannot replace each other. For example, NGINX proxy_read_timeout constrains the wait between consecutive reads, not the maximum duration of the entire response. NGINX proxy timeout
After the first attempt has already taken 220ms, the second attempt cannot reset the full 850ms budget again. You must also consider proxy queuing, response return, and the client's remaining wait time. Whether the backend task is cancelled after a timeout depends on whether the cancellation signal continues to propagate to the application and downstream.
Retriable conditions must be met simultaneously: the request body must be replayable, the failure phase must allow it, business execution uncertainty must be acceptable, and the remaining budget must be sufficient. After partially sending a response to the client, it is usually not possible to transparently switch to another backend to replay the entire response. POST may rely on explicit idempotency keys; "receiving a 502" does not automatically prove safe repetition.
budget, elapsed, first_attempt, reserve=850, 140, 220, 100remaining=max(0,budget-elapsed-first_attempt-reserve)assertremaining==390# Teaching model: independent failure probability p per attempt, max 3 attempts.
p=0.2expected_attempts=1+p+p**2assertabs(expected_attempts-1.24)<1e-12assertabs(1000*expected_attempts-1240)<1e-9# Three layers, max 3 attempts each, worst case amplifies to 27 downstream attempts.
assert3**3==27print('Remaining ms / Expected attempts per sec / Nested worst attempts:',remaining,1000*expected_attempts,3**3)
The independent failure above is just a teaching assumption; real failures are often correlated, and retries may further collapse dependencies. Limiting the number of attempts, total duration, and additional retry traffic, and using appropriate backoff and jitter, is necessary to avoid amplification from layered retries.
How Much Longer Does It Take to Reset the Full Timeout for Each Retry?
The time bars share the same start point and deadline; the first failure does not shift them. The "available budget" here only answers whether there is still time, not whether it is safe to repeat the business operation; retries should only occur when both judgments are passed.
Preparing the visual
How Much Longer Does It Take to Reset the Full Timeout for Each Retry?
The retry budget is the balance of the total deadline, not starting a new complete time window. When the balance is zero, no amount of retry configuration will generate time.
{"id":"proxy-retry-deadline","title":"How Much Longer Does It Take to Reset the Full Timeout for Each Retry?","summary":"The retry budget is the balance of the total deadline, not starting a new complete time window. When the balance is zero, no amount of retry configuration will generate time.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"prior\" data-i18n=\"prior\"></label><output for=\"prior\" id=\"prior-value\"></output><input id=\"prior\" type=\"range\" min=\"0\" max=\"500\" value=\"140\" step=\"10\"></div><div class=\"control\"><label for=\"first\" data-i18n=\"first\"></label><output for=\"first\" id=\"first-value\"></output><input id=\"first\" type=\"range\" min=\"0\" max=\"850\" value=\"220\" step=\"10\"></div><div class=\"control\"><label for=\"reserve\" data-i18n=\"reserve\"></label><output for=\"reserve\" id=\"reserve-value\"></output><input id=\"reserve\" type=\"range\" min=\"0\" max=\"300\" value=\"100\" step=\"10\"></div><div id=\"timeline\" class=\"intervals\"></div><div class=\"metrics\"><div><span data-i18n=\"remaining\"></span><strong id=\"remaining\"></strong></div><div><span data-i18n=\"wrong\"></span><strong id=\"wrong\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const p=+$('#prior').value,f=+$('#first').value,r=+$('#reserve').value,left=Math.max(0,850-p-f-r);for(const id of ['prior','first','reserve'])set(id+'-value',$('#'+id).value);intervals('timeline',[{start:0,end:850,label:'deadline 850 ms'},{start:0,end:p+f,label:'elapsed '+(p+f),cls:'gold'},{start:p+f,end:p+f+left,label:'retry '+left}],1650);set('remaining',left);set('wrong',p+f+850+r);set('equation','remaining = max(0,850 − '+p+' − '+f+' − '+r+') = '+left);$('#remaining').dataset.value=left;}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"first":"First Attempt Duration (ms)","heading":"How Much Longer Does It Take to Reset the Full Timeout for Each Retry?","intro":"Adjust elapsed time, first attempt, and return reserve to compare against the shared deadline and the second budget after error reset.","prior":"Elapsed Before Attempt (ms)","remaining":"Second Available Budget (ms)","reserve":"Return Reserve (ms)","reset":"Reset","scope":"Total budget 850 ms, serial two attempts; only checks time permission, business idempotency, failure phase, cancellation, and request replayability still need to be met independently.","scopeLabel":"Model Scope and Conventions","wrong":"If resetting to 850ms, Total Time Limit"}}
Buffering, Streaming, and Backpressure
Response buffering allows the proxy to read the upstream faster, cache the content, and slowly send it to the client, reducing long-term occupation of the upstream by slow readers; the cost is memory/disk budget and potential first-byte or chunking latency. Request buffering affects how slow uploads occupy the backend and whether the request body can be replayed after failure.
SSE, model streaming output, and large downloads need flushing configured according to delivery requirements. Relying solely on the application calling flush does not guarantee that every proxy and compressor outputs immediately; turning off all buffering is not a free optimization, as slow clients may propagate backpressure upstream and occupy resources for a long time. NGINX proxy buffering
Connection pool reuse reduces handshakes, but you must coordinate the lifespan of idle upstream connections. Pool connections that the backend has already closed but the proxy has not yet detected will cause failures upon reuse; whether to retry still depends on request semantics and the sending phase, and infinite retries cannot mask pool configuration issues.
Health Checks and Publish Drain
Active checks periodically send probes, while passive checks observe real request failures; the two answer different questions. /readyz returning 200 can indicate that this instance is ready to receive new work, but should not unconditionally diffuse transient jitter of all shared dependencies into the simultaneous removal of the entire instance pool.
Detection period, failure threshold, and probe timeout jointly determine discovery latency. Probing every 10 seconds and removing only after two consecutive failures does not mean the instance is cut off exactly 20 seconds after a failure occurs; it is also affected by occurrence phase, probe duration, and scheduling. Upon recovery, traffic can be gradually increased, and caches or connections can be warmed up.
Decommissioning first stops accepting new work, then waits for already accepted requests to finish, and finally closes residual connections at the deadline. HTTP/2 and HTTP/3 can use the respective GOAWAY, while long connections like WebSockets/SSE need explicit maximum drain time and client reconnection behavior. Simply removing the instance from the discovery list will not make all existing connections disappear immediately.
Blue-green or weight-based canary deployments also need to consider session affinity, request cost, and sample distribution. 1% of requests do not necessarily cover 1% of users, and may miss large tenants or specific paths; metrics should be observed by version, route, and error type simultaneously.
Observability: Mapping a Failure to a Specific Attempt
Access logs should at least be able to associate request identifiers, routes, selected upstream, attempt count, status, and key latencies. Breaking down the client's total latency into queuing, connection setup, waiting for headers, and response transmission can distinguish between "upstream didn't respond," "client read slowly," and "proxy pool waiting."
Caching, rate limiting, and authorization should also indicate which layer executed them; if a cache hit bypasses the backend, the backend log having no request is normal. Do not mix user tokens, original certificate private keys, or sensitive content into debug logs; diagnostics should rely on structured results and necessary identifiers.
Continue Reading
mTLS: Identity boundaries after proxy termination.
HTTP/2, HTTP/3: Multiplexing, exit, and flow control.