Solid walk through of upstream pooling. The ops failure mode we still hit is treating the pool as forever-healthy once it accepts TCP: without max_fails and fail_timeout matched to real probe latency, a peer that hangs past proxy_read_timeout keeps collecting traffic while others sit idle. Prefer least_conn once replica sizes are equal, keep proxy_next_upstream limited to error/timeout on safe methods only, and pin proxy_http_version 1.1 with Connection cleared so keepalive actually reuses sockets into the pool.
After a rolling restart, log $upstream_addr and $upstream_status together and confirm a draining peer leaves within one fail_timeout window instead of sprinkling intermittent 502s. If you add weight, re-check under equal load — unequal weights plus keepalive can pin sticky-looking latency that looks like app slowness. Hit each member directly on the private network, then through the upstream name, before trusting the pool in production.
Solid walk through of upstream pooling. The ops failure mode we still hit is treating the pool as forever-healthy once it accepts TCP: without max_fails and fail_timeout matched to real probe latency, a peer that hangs past proxy_read_timeout keeps collecting traffic while others sit idle. Prefer least_conn once replica sizes are equal, keep proxy_next_upstream limited to error/timeout on safe methods only, and pin proxy_http_version 1.1 with Connection cleared so keepalive actually reuses sockets into the pool.
After a rolling restart, log $upstream_addr and $upstream_status together and confirm a draining peer leaves within one fail_timeout window instead of sprinkling intermittent 502s. If you add weight, re-check under equal load — unequal weights plus keepalive can pin sticky-looking latency that looks like app slowness. Hit each member directly on the private network, then through the upstream name, before trusting the pool in production.