Agree that TCP accept is a weak health signal. When nginx sits in front of Node replicas we treat passive upstream health separately from the proxy's own liveness: max_fails plus fail_timeout only mark a peer down after real request failures, so a process that still accepts sockets but returns 5xx or hangs past proxy_read_timeout actually leaves the pool. Pair that with a cheap app path that checks event-loop lag or a critical dependency, not just listen(), and keep the proxy health endpoint thin so orchestrators do not confuse "nginx is up" with "backends can serve".
Ops practice: log upstream status and response time on the proxy, and after a rolling restart confirm traffic shifts off a draining peer within one fail_timeout window instead of sprinkling 502s. Probe via the docker/service name on the overlay, not the public TLS name, so cert or SNI noise does not fake an unhealthy backend. Passive flags alone will not catch concurrency-only breaks — add a short synthetic check under load in CI for those.
Agree that TCP accept is a weak health signal. When nginx sits in front of Node replicas we treat passive upstream health separately from the proxy's own liveness: max_fails plus fail_timeout only mark a peer down after real request failures, so a process that still accepts sockets but returns 5xx or hangs past proxy_read_timeout actually leaves the pool. Pair that with a cheap app path that checks event-loop lag or a critical dependency, not just listen(), and keep the proxy health endpoint thin so orchestrators do not confuse "nginx is up" with "backends can serve".
Ops practice: log upstream status and response time on the proxy, and after a rolling restart confirm traffic shifts off a draining peer within one fail_timeout window instead of sprinkling 502s. Probe via the docker/service name on the overlay, not the public TLS name, so cert or SNI noise does not fake an unhealthy backend. Passive flags alone will not catch concurrency-only breaks — add a short synthetic check under load in CI for those.