load-balancer

v2026.09.24

Load balancing with Nginx and HAProxy. Covers upstream configuration, balancing algorithms, health checks, SSL termination, session persistence, and monitoring. USE WHEN: user mentions "load balancer", "nginx upstream", "haproxy", "nginx load balancing", "round robin", "least_conn", "ip_hash", "haproxy backend", "haproxy frontend", "haproxy acl", "haproxy stats", "ssl termination", "sticky session", "upstream health check", "nginx proxy", "haproxy health check" DO NOT USE FOR: Caddy reverse proxy — use `caddy` skill, Traefik service mesh — use `traefik` skill, Kubernetes Ingress or Service resources — use `kubernetes` skill, DNS-level load balancing (Route 53, Cloudflare LB)

GitHub
Install command
npx skhub add claude-dev-suite/load-balancer
Markdown
SKILL.md

Load Balancing: Nginx & HAProxy Core Knowledge

Nginx vs HAProxy — When to Use Each

DimensionNginxHAProxy
Primary roleWeb server + reverse proxy + LBDedicated load balancer + proxy
Config complexityLow-mediumMedium-high
HTTP modesHTTP/1.1, HTTP/2HTTP/1.1, HTTP/2 (enterprise), HTTP/3 (1.9+)
TCP/UDP LBNginx Plus or stream moduleNative, very mature
Active health checksNginx Plus only (open-source: passive only)Built-in, free
Stats/metrics UIThird-party (nginx-lua, stub_status)Built-in stats page
Sticky sessionsNginx Plus (cookie) or ip_hashStick tables (any key), free
Connection reuseKeepalive to upstreamReuse connections, queue management
Dynamic reconfigurationNginx Plus (upstream_conf API)Runtime API (HAProxy 2.0+)
Ecosystem / docsVery mature, massiveMature, industry standard for pure LB

Use Nginx when: you already use Nginx as your web server, you want a single tool for serving files + proxying + LB, or your team knows Nginx.

Use HAProxy when: you need advanced health checks, fine-grained ACL routing, TCP load balancing, or maximum LB performance and observability.


Nginx Upstream Configuration

Basic Upstream Block

# /etc/nginx/nginx.conf or included conf

http {
    # Shared memory zone for upstream state across workers
    # Required for proper load balancing with multiple workers
    upstream app_backend {
        zone app_zone 256k;         # Shared state (round_robin works without it too)

        # Balancing method (default is round_robin if nothing specified)
        # least_conn;               # Route to backend with fewest active connections
        # ip_hash;                  # Sticky: same client IP always → same backend
        # hash $request_uri consistent;  # Consistent hashing by URI (good for caching)
        # random two least_conn;    # Pick 2 random servers, send to less-loaded one

        server 10.0.1.10:3000 weight=3 max_fails=3 fail_timeout=30s;
        server 10.0.1.11:3000 weight=1 max_fails=3 fail_timeout=30s;
        server 10.0.1.12:3000 weight=1 max_fails=3 fail_timeout=30s;

        # Backup server — only used when all primaries are down
        server 10.0.1.20:3000 backup;

        # Permanently excluded (maintenance)
        # server 10.0.1.13:3000 down;

        # Keepalive connections to upstream (dramatically reduces TCP overhead)
        keepalive 64;               # Max idle keepalive connections per worker
        keepalive_requests 1000;    # Max requests per keepalive connection
        keepalive_timeout 60s;
    }

    server {
        listen 80;
        server_name api.example.com;

        # Logging with upstream info
        log_format upstream_log '$remote_addr - $upstream_addr [$time_local] '
                                 '"$request" $status $body_bytes_sent '
                                 'rt=$request_time urt=$upstream_response_time';
        access_log /var/log/nginx/api_access.log upstream_log;

        location / {
            proxy_pass         http://app_backend;
            proxy_http_version 1.1;                     # Required for keepalive
            proxy_set_header   Connection "";           # Required for keepalive
            proxy_set_header   Host              $host;
            proxy_set_header   X-Real-IP         $remote_addr;
            proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto $scheme;

            # Timeouts
            proxy_connect_timeout  5s;
            proxy_send_timeout     30s;
            proxy_read_timeout     30s;

            # Passive health check: try next upstream on errors
            proxy_next_upstream     error timeout http_502 http_503 http_504;
            proxy_next_upstream_tries 3;
            proxy_next_upstream_timeout 10s;

            # Buffering
            proxy_buffering    on;
            proxy_buffer_size  16k;
            proxy_buffers      8 16k;
        }

        # Health check endpoint for external monitors
        location /nginx-health {
            access_log off;
            return 200 "OK\n";
        }
    }
}

Active Health Check Workaround (Open-Source Nginx)

Nginx OSS only supports passive health checks. Simulate active checks with a small service or use the nginx_upstream_check_module (third-party).

# Install lua-nginx-module + lua-resty-upstream-healthcheck
# OR use OpenResty (Nginx + LuaJIT bundle)
# Simple approach: use a separate monitoring tool (Consul, HAProxy) alongside Nginx

SSL Termination at Nginx

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # Certificate (from Let's Encrypt / Certbot)
    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    # Modern TLS settings
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # HSTS
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    location / {
        proxy_pass http://app_backend;      # Plain HTTP to backend (internal network)
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# HTTP → HTTPS redirect
server {
    listen 80;
    server_name api.example.com;
    return 301 https://$host$request_uri;
}

Stub Status (Metrics Endpoint)

server {
    listen 127.0.0.1:8080;   # Bind to localhost only
    location /nginx_status {
        stub_status;
        allow 127.0.0.1;
        deny all;
    }
}

HAProxy Configuration

Full HTTP Load Balancer Config

# /etc/haproxy/haproxy.cfg

global
    log         /dev/log local0 info
    log         /dev/log local0 notice notice
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     50000               # Total concurrent connections
    user        haproxy
    group       haproxy
    daemon
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners

defaults
    log         global
    mode        http                # http | tcp
    option      httplog             # Structured HTTP log format
    option      dontlognull         # Don't log health checks
    option      forwardfor          # Add X-Forwarded-For header
    option      http-server-close   # Close server-side connection after each request
    option      redispatch          # Retry on different server if session fails
    timeout     connect  5s
    timeout     client   30s
    timeout     server   30s
    timeout     http-request 10s    # Max time to receive full HTTP request
    timeout     http-keep-alive 5s
    timeout     queue   1m          # Max wait in queue when all servers full
    timeout     tunnel  1h          # For WebSocket / long-lived connections
    retries     3

#──────────────────────────────────────
# Stats page
#──────────────────────────────────────
frontend stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 10s
    stats auth admin:strongpassword    # CHANGE THIS
    stats show-legends
    stats show-node
    # Restrict to internal IPs
    acl internal_nets src 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
    tcp-request connection reject if !internal_nets

#──────────────────────────────────────
# HTTPS frontend (SSL termination)
#──────────────────────────────────────
frontend https_in
    bind *:443 ssl crt /etc/ssl/certs/example.com.pem  # Combined cert+key PEM
    bind *:80
    http-request redirect scheme https unless { ssl_fc }

    # Define ACLs for routing
    acl host_api   hdr(host) -i api.example.com
    acl host_app   hdr(host) -i app.example.com
    acl path_admin path_beg /admin

    # Security headers
    http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    http-response set-header X-Content-Type-Options    nosniff
    http-response set-header X-Frame-Options           DENY
    http-response del-header Server

    # ACL-based routing to backends
    use_backend api_servers  if host_api
    use_backend app_servers  if host_app !path_admin
    use_backend admin_server if host_app path_admin

    default_backend app_servers

#──────────────────────────────────────
# API backend
#──────────────────────────────────────
backend api_servers
    balance leastconn               # roundrobin | leastconn | source | uri | random

    # Active HTTP health checks
    option httpchk GET /health HTTP/1.1\r\nHost:\ api.example.com
    http-check expect status 200
    default-server inter 10s fastinter 2s downinter 5s rise 2 fall 3

    # Connection limits per server
    default-server maxconn 100 maxqueue 50

    # Keepalive to backends
    option http-server-close
    timeout connect 3s
    timeout server  15s

    server api1 10.0.1.10:3000 check weight 10
    server api2 10.0.1.11:3000 check weight 10
    server api3 10.0.1.12:3000 check weight 5   # Lower weight — less powerful
    server api_backup 10.0.1.20:3000 check backup

#──────────────────────────────────────
# App backend with sticky sessions
#──────────────────────────────────────
backend app_servers
    balance roundrobin
    option httpchk GET /health
    http-check expect status 200

    # Cookie-based sticky sessions
    cookie SERVERID insert indirect nocache httponly secure

    default-server inter 10s rise 2 fall 3
    server app1 10.0.1.30:8080 check cookie app1
    server app2 10.0.1.31:8080 check cookie app2
    server app3 10.0.1.32:8080 check cookie app3

#──────────────────────────────────────
# Admin backend — IP restricted
#──────────────────────────────────────
backend admin_server
    # IP whitelist using TCP-request (set in frontend ACL)
    option httpchk GET /admin/health
    server admin1 10.0.1.50:8080 check

#──────────────────────────────────────
# TCP mode example (e.g., PostgreSQL)
#──────────────────────────────────────
frontend postgres_in
    bind *:5432
    mode tcp
    default_backend postgres_servers

backend postgres_servers
    mode tcp
    balance leastconn
    option tcp-check
    server pg_primary 10.0.2.10:5432 check
    server pg_replica 10.0.2.11:5432 check backup

HAProxy Runtime API

# Enable in global section: stats socket /run/haproxy/admin.sock mode 660 level admin

# Show current server states
echo "show servers state" | socat stdio /run/haproxy/admin.sock

# Drain a server (stop sending new requests, finish existing)
echo "set server api_servers/api1 state drain" | socat stdio /run/haproxy/admin.sock

# Bring server back online
echo "set server api_servers/api1 state ready" | socat stdio /run/haproxy/admin.sock

# Change weight dynamically
echo "set server api_servers/api2 weight 20" | socat stdio /run/haproxy/admin.sock

# Show backend health
echo "show health" | socat stdio /run/haproxy/admin.sock

Error Pages

errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http

Anti-Patterns

Anti-PatternProblemSolution
No health checks (Nginx passive only, never configured)Dead backends receive traffic → client errorsConfigure proxy_next_upstream in Nginx; use option httpchk in HAProxy
ip_hash with clients behind shared NAT / CDNUneven distribution — all clients from same office go to one serverUse least_conn or cookie-based stickiness instead of IP hash
No proxy_http_version 1.1 + Connection "" with Nginx keepaliveKeepalive not actually enabled — new TCP connection per requestAlways pair proxy_http_version 1.1 with proxy_set_header Connection ""
Setting timeout client 30s for WebSocket connectionsWebSocket connections dropped after 30 seconds idleUse timeout tunnel 1h (HAProxy) or proxy_read_timeout 0 (Nginx) for WS paths
Not logging $upstream_addr and $upstream_response_timeCan't diagnose which backend is slowAdd to Nginx log_format; use HAProxy %b/%s log variables
maxconn not tuned in HAProxy globalHAProxy queues or rejects connections under loadSet maxconn based on RAM: ~1 MB per 1000 connections; adjust per server too
No proxy_buffering tuning in NginxSlow clients cause upstream to waitKeep proxy_buffering on; tune proxy_buffers for your response sizes
Nginx upstream without zone directiveRound-robin per-worker only, no true least_conn across workersAlways add zone <name> 256k to upstream block
HAProxy stats page exposed on public interfaceStats reveal server IPs, health, and allow admin actionsBind stats to 127.0.0.1 or restrict with ACL src 10.0.0.0/8
TLS termination without ssl_session_cacheFull TLS handshake on every request → high CPUAdd ssl_session_cache shared:SSL:10m and ssl_session_timeout 1d

Troubleshooting

SymptomLikely CauseFix
Uneven traffic distribution with least_connSingle Nginx worker handles one backend; workers share if zone is setAdd zone directive to upstream block for shared state
Backend marked down immediatelyHealth check URL returns non-200 or times outcurl http://10.0.1.10:3000/health from load balancer host; adjust rise/fall thresholds
502 Bad Gateway on all requestsAll backends down or proxy_pass pointing to wrong addressCheck backend process; verify port; test curl backend_ip:port from LB
Session drops when scaling backendsNo sticky sessions configuredAdd ip_hash (Nginx) or cookie directive (HAProxy)
Keepalive not working (new TCP per request)Missing proxy_http_version 1.1 or Connection "" headerAdd both headers; verify with `netstat -an
HAProxy shows "no server available"All servers DOWN in health checks`echo "show servers state"
Nginx returns 504 (gateway timeout)Backend too slow; proxy_read_timeout too shortIncrease proxy_read_timeout; investigate backend performance
SSL handshake errorsCipher mismatch or TLS version too oldCheck client TLS support; ensure ssl_protocols TLSv1.2 TLSv1.3
HAProxy rate higher than expected CPUToo many health check connectionsIncrease inter interval: inter 30s for stable backends
X-Forwarded-For shows load balancer IPoption forwardfor not set (HAProxy) or proxy_set_header X-Forwarded-For missing (Nginx)Add the respective directive; restart LB

Production Checklist

Nginx:

  • zone directive in all upstream blocks
  • proxy_http_version 1.1 + proxy_set_header Connection ""
  • proxy_next_upstream with appropriate error codes
  • X-Real-IP and X-Forwarded-For headers set
  • keepalive set on upstream block
  • $upstream_addr and $upstream_response_time in access log format
  • SSL session cache and modern TLS settings

HAProxy:

  • option httpchk on all backends with correct URL and Host header
  • inter, rise, fall tuned for your SLO
  • maxconn set globally and per server
  • Stats page on internal interface with auth
  • Runtime API socket configured
  • timeout tunnel set for WebSocket backends
  • Error files configured for all 4xx/5xx codes
  • Log format includes backend server name
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/infrastructure/load-balancer

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1