Nginx as a Load Balancer
Introduction¶
Beyond reverse-proxying to one backend, Nginx load-balances across a pool with
its upstream block. Open-source Nginx does this well for HTTP and, with the
stream module, for raw TCP/UDP.
The upstream block¶
upstream app {
least_conn;
server 10.0.1.11:8080 max_fails=3 fail_timeout=15s weight=2;
server 10.0.1.12:8080 max_fails=3 fail_timeout=15s;
server 10.0.1.13:8080 backup; # only used when others are down
keepalive 32; # persistent connections to upstreams
}
server {
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection ""; # required for upstream keepalive
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
proxy_connect_timeout 2s;
proxy_read_timeout 30s;
}
}
Balancing methods¶
| Method | Behaviour | Use |
|---|---|---|
| (default) round-robin | rotate, weighted | stateless backends |
least_conn |
fewest active connections | uneven / long-lived requests |
ip_hash |
client IP → fixed backend | crude session stickiness (no cookie support needed) |
hash $key consistent |
hash an arbitrary key, minimal reshuffle on change | cache affinity (hash $request_uri consistent) |
random two least_conn |
pick 2 at random, use the less loaded | large pools, avoids herd on one node |
ip_hash breaks behind a CDN/NAT (many clients → one IP → one backend). Prefer
stateless apps; if you need stickiness, use hash on a cookie
(hash $cookie_sessionid consistent) or offload sessions to Redis.
Health checks¶
- Open-source Nginx: passive only.
max_failsfailed responses withinfail_timeoutmarks a server down; afterfail_timeoutit's retried. There's no active probe — a server that's up but returning garbage stays in rotation until it producesmax_failserrors. - Nginx Plus (or the third-party
nginx_upstream_check_module): activehealth_checkwith an interval, a URI, and expected status/body.
For real active checks on open-source Nginx, put HAProxy in front of the pool, or use the check module, or rely on your orchestrator (Kubernetes readiness) to pull bad backends.
Retries and proxy_next_upstream¶
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
If a request to one backend fails with a listed condition, Nginx retries the
next backend. Be careful retrying non-idempotent requests — by default
Nginx does NOT retry non-GET requests after they've sent a body unless
non_idempotent is added. Don't add it unless your backend is truly idempotent.
Upstream keepalive¶
Without keepalive, Nginx opens a new TCP (and TLS) connection to the backend
for every request — a lot of overhead at scale. The keepalive 32; +
proxy_http_version 1.1; + proxy_set_header Connection ""; trio maintains a
pool of persistent connections per worker.
TCP/UDP (L4) with the stream module¶
stream {
upstream db {
least_conn;
server 10.0.2.11:5432 max_fails=2 fail_timeout=10s;
server 10.0.2.12:5432 max_fails=2 fail_timeout=10s;
}
server {
listen 5432;
proxy_pass db;
proxy_connect_timeout 1s;
proxy_timeout 1h; # idle timeout for long-lived DB connections
}
# UDP (e.g. DNS, syslog)
server {
listen 53 udp;
proxy_pass dns_pool;
proxy_responses 1;
}
}
Verification and troubleshooting¶
nginx -T | grep -A10 'upstream app'
curl -s -o /dev/null -w '%{http_code} %{remote_ip}\n' http://lb/ -H 'X-Debug: 1'
# add to the location for debugging:
# add_header X-Upstream $upstream_addr always;
# add_header X-Upstream-Status $upstream_status always;
tail -f /var/log/nginx/error.log # "no live upstreams", "upstream timed out"
502 Bad Gatewayintermittently — a backend is up but slow/erroring; passive health check hasn't marked it down yet, ormax_failsis 0 (disables the check — never do that). Lowermax_fails, addproxy_next_upstream.no live upstreams while connecting to upstream— all backends marked failed (a transient issue trippedmax_failson all of them, or they're genuinely down). Nginx retries afterfail_timeout. Consider abackupserver.- All traffic hitting one backend —
ip_hashbehind a NAT/CDN, or ahashkey with low cardinality. Switch toleast_conn/round-robin or hash a higher-cardinality key. - New connection per request to the backend —
keepalivemissing, orproxy_http_versionstill 1.0, orConnectionheader passed through. All three lines are required. - Non-idempotent request executed twice —
proxy_next_upstreamretried a POST. Removenon_idempotent, or make the endpoint idempotent, or don't retry POSTs. - L4: DB connections drop after 10 min —
proxy_timeout(default 10m idle); raise it for long-lived pooled connections. - Weighted distribution not matching weights —
least_connignores weight proportions somewhat under uneven load; round-robin honours weights strictly.
Related tools and reading¶
- On-site: Port Checker, HTTP Header Analyzer.
- Related posts: HAProxy basics, An Nginx reverse proxy with TLS.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Web & Mail Servers
Running BIND 9 as an Authoritative Server
BIND as an authoritative server: zone files, secured AXFR with TSIG, SOA serial rules, and DNSSEC inline-signing. Here's a working master/slave setup.
October 24, 2026Varnish Cache in Front of a Site
Varnish is a caching HTTP accelerator configured in VCL. Here's the request lifecycle, why you strip cookies, how bans work, and grace mode for serving stale during backend trouble.
October 24, 2026Let's Encrypt with Certbot, Including Renewal That Doesn't Break
Getting the first cert is easy; keeping renewal working for years is the part people get wrong. Webroot vs DNS-01, deploy hooks, and the failure modes.
October 24, 2026