Skip to content

Load Balancing with HAProxy

A reverse proxy routes traffic to one upstream; a load balancer spreads traffic across many identical upstreams so you can scale out and survive the loss of any single instance. HAProxy is the reference open-source load balancer — extremely fast, with deep health-checking and observability.

If you only have a single app instance, you want a reverse proxy (nginx or Traefik), not this. Reach for a load balancer when one instance can’t carry the load or when downtime of a single instance is unacceptable.

HAProxy can balance at two layers, and the choice drives everything else:

Layer 4 (TCP)Layer 7 (HTTP)
SeesIP + port onlyFull HTTP — host, path, headers, cookies
Can route by URL/hostNoYes
Can inject/read cookiesNoYes (needed for app-cookie stickiness)
TLSPass-through or terminateTerminate to inspect
OverheadLowerSlightly higher

Use L4 for raw TCP services (databases, SMTP, or TLS pass-through). Use L7 when you need host/path routing, header manipulation, or cookie-based sticky sessions.

/etc/haproxy/haproxy.cfg
global
maxconn 20000
log /dev/log local0
defaults
mode http
timeout connect 5s
timeout client 50s
timeout server 50s
option httplog
frontend web
bind :443 ssl crt /etc/haproxy/certs/app.pem
default_backend app_servers
backend app_servers
balance roundrobin
option httpchk GET /health # active health check
http-check expect status 200
server app1 10.0.0.11:8080 check
server app2 10.0.0.12:8080 check
server app3 10.0.0.13:8080 check backup # only used if others are down

The whole point of a load balancer is to stop sending traffic to a broken instance. HAProxy marks a server DOWN after failed checks and UP again once it recovers:

  • option httpchk GET /health + http-check expect status 200 — application-level check; far better than a bare TCP connect, because an app can accept connections while being unable to serve.
  • Tune with server ... check inter 2s fall 3 rise 2 (check every 2s; down after 3 failures, back up after 2 successes).

If your app keeps per-user state in memory, a user must keep hitting the same backend. Two approaches:

# Cookie-based (L7) — HAProxy inserts a cookie naming the chosen server
backend app_servers
balance roundrobin
cookie SRV insert indirect nocache
server app1 10.0.0.11:8080 check cookie a1
server app2 10.0.0.12:8080 check cookie a2
# Source-IP based (works at L4) — same client IP -> same server
backend app_servers
balance source

A single HAProxy is itself a single point of failure. Run two and float a virtual IP (VIP) between them with Keepalived (VRRP). DNS points at the VIP; if the primary dies, the backup claims the VIP within seconds.

# /etc/keepalived/keepalived.conf (primary)
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 200 # backup uses a lower priority (e.g. 100)
virtual_ipaddress {
10.0.0.10/24 # the VIP clients connect to
}
track_script { chk_haproxy } # demote if local HAProxy is down
}
HAProxynginxTraefik
Primary roleLoad balancerReverse proxy / web serverContainer-native proxy
L4 (TCP) balancingExcellentGood (stream)Limited
Health checkingDeepestBasic (open-source)Good
Auto certsNoNo (Certbot)Yes
Config styleStatic fileStatic fileDocker labels

For most self-hosted setups, nginx or Traefik is enough. Add HAProxy when you specifically need its load-balancing and health-checking depth, or L4 balancing.