Load balancing cheatsheet.
Upstream
upstream app {
server 10.0.0.1:8000;
server 10.0.0.2:8000;
server 10.0.0.3:8000;
}
server {
location / { proxy_pass http://app; }
}
Algorithms
upstream app {
# default: round-robin
least_conn; # fewest active
ip_hash; # sticky by client IP
hash $request_uri consistent; # consistent by URI
random two least_conn; # power-of-two-choices
}
Weights
upstream app {
server a.example.com weight=3;
server b.example.com weight=1;
}
3:1 distribution.
Backup server
upstream app {
server primary;
server secondary backup;
}
Backup used only when primaries fail.
down
upstream app {
server a;
server b down; # temporarily disabled
}
Passive health checks
upstream app {
server 10.0.0.1 max_fails=3 fail_timeout=30s;
server 10.0.0.2 max_fails=3 fail_timeout=30s;
}
3 consecutive failures → mark unavailable for 30s.
What counts as failure
location / {
proxy_pass http://app;
proxy_next_upstream error timeout http_502 http_503 http_504;
}
Active health checks (Nginx Plus only)
upstream app {
zone app 64k;
server 10.0.0.1;
}
location / {
proxy_pass http://app;
health_check interval=10 fails=3 passes=2 uri=/health;
}
Open source: passive only.
Keepalive to backend
upstream app {
server 10.0.0.1:8000;
keepalive 32;
keepalive_requests 1000;
keepalive_timeout 60s;
}
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
Critical for performance — avoids per-request TCP/TLS overhead.
Slow start (Nginx Plus)
server a slow_start=30s;
Gradually ramp traffic after server recovers.
Sticky sessions
ip_hash: by IP, but breaks behind NAT.
hash $cookie_jsessionid: sticky by cookie.
Better: Nginx Plus sticky directive (route, cookie, learn).
Failover (active-passive)
upstream app {
server primary.example.com:8000 max_fails=2 fail_timeout=10s;
server failover.example.com:8000 backup;
}
Retries
proxy_next_upstream error timeout invalid_header http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 30s;
Try next upstream up to 3 times within 30s.
Health endpoint pattern
Backend exposes /health returning 200 if OK. Nginx checks via max_fails on the proxied path (passive).
Read multiple backends per request
upstream search {
server search-1;
server search-2;
server search-3;
}
Can’t natively merge responses; do that in app.
Cross-region
upstream app_us {
server us-1;
server us-2;
}
upstream app_eu {
server eu-1;
server eu-2;
}
map $geoip2_data_country_code $upstream {
default app_us;
US app_us;
DE app_eu;
FR app_eu;
}
location / {
proxy_pass http://$upstream;
}
Requires geoip2 module.
TCP / UDP load balancing (stream block)
stream {
upstream pg {
server 10.0.0.1:5432;
server 10.0.0.2:5432;
}
server {
listen 5432;
proxy_pass pg;
}
}
For non-HTTP services.
L7 routing examples
location /v1/ {
proxy_pass http://app_v1;
}
location /v2/ {
proxy_pass http://app_v2;
}
# Path-based to different upstreams
location ~ ^/api/(users|orders)/ {
proxy_pass http://app_users;
}
location /api/payments/ {
proxy_pass http://app_payments;
}
Canary by header
map $http_x_canary $upstream {
default app;
"1" app_canary;
}
upstream app { server stable; }
upstream app_canary { server canary; }
location / {
proxy_pass http://$upstream;
}
Common mistakes
- Forgetting
keepalive→ connection storm. ip_hashbehind shared NAT → uneven distribution.- No health check semantics → broken backends still get traffic.
- Same
max_failsfor many flaky upstreams → flapping. - Sticky session as substitute for shared session storage.
Read this next
If you want my upstream + LB templates, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .