HAProxy: Load Balancing and Health Checking for Self-Hosted Services
HAProxy distributes traffic across backend servers. Unlike a web server that also proxies, it does one job, and the health checking and statistics are where it shows.
sudo apt install haproxy # Debian and Ubuntu
sudo dnf install haproxy # Fedora
A working configuration
# /etc/haproxy/haproxy.cfg
global
log /dev/log local0
maxconn 4096
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
frontend web
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
http-request redirect scheme https unless { ssl_fc }
default_backend app_servers
backend app_servers
balance roundrobin
option httpchk GET /health
http-check expect status 200
server app1 10.0.1.10:8080 check inter 5s fall 3 rise 2
server app2 10.0.1.11:8080 check inter 5s fall 3 rise 2
listen stats
bind *:8404
stats enable
stats uri /
stats refresh 10s
sudo haproxy -c -f /etc/haproxy/haproxy.cfg # validate before reloading
sudo systemctl reload haproxy
Always validate. A syntax error on reload takes the proxy down, and with it everything behind it.
Health checks are the point
option httpchk GET /health
http-check expect status 200
server app1 10.0.1.10:8080 check inter 5s fall 3 rise 2
inter 5s checks every five seconds. fall 3 removes a backend after three consecutive failures. rise 2 returns it after two successes.
Those numbers matter. Too aggressive and a backend under momentary load gets pulled out, which increases load on the rest and can cascade. Too lax and users hit a dead backend for a minute.
Point checks at a cheap dedicated endpoint. A /health that returns 200 immediately is right. A check hitting your homepage, which queries a database, will fail exactly when the system is busiest.
Balancing algorithms
balance roundrobin # even distribution, the default
balance leastconn # fewest active connections
balance source # hash the client IP, sticky
balance uri # hash the URI, good for cache locality
roundrobin for uniform backends and short requests.
leastconn when request durations vary a lot, such as WebSockets or long polling, because round-robin will pile long connections onto one server.
source when clients must return to the same backend and you have no shared session store. It is a workaround for missing shared state rather than a design goal.
The statistics page
listen stats
bind *:8404
stats enable
stats uri /
stats auth admin:changeme
Live per-backend status, current and total sessions, bytes, errors, and check results. It is genuinely the best thing about running HAProxy: when something is wrong, this page usually says what.
Put it behind authentication and do not expose it publicly.
TCP mode
For anything that is not HTTP:
listen postgres
bind *:5432
mode tcp
option tcp-check
balance leastconn
server db1 10.0.2.10:5432 check
server db2 10.0.2.11:5432 check backup
mode tcp forwards bytes without parsing them, so you lose hostname routing and header manipulation and gain the ability to proxy databases, SSH, and message brokers.
backup marks a server used only when the primaries are down, which is how you do active-passive failover.
HAProxy or nginx
nginx or Caddy for one backend, static files, and TLS. Simpler, and Caddy handles certificates automatically.
HAProxy for several backends, real health checking, non-HTTP protocols, or when you want the statistics.
Running both is common and reasonable: HAProxy at the edge distributing across nodes, nginx on each node serving the application. Our reverse proxy guide covers the concepts, and note that forwarded headers matter here too:
option forwardfor
http-request set-header X-Forwarded-Proto https if { ssl_fc }
Without those, backends see the proxy as the client and generate the wrong URLs, exactly as with nginx.
Certificates
HAProxy wants the certificate and key concatenated into one file:
cat /etc/letsencrypt/live/example.com/fullchain.pem \
/etc/letsencrypt/live/example.com/privkey.pem \
> /etc/haproxy/certs/example.com.pem
chmod 600 /etc/haproxy/certs/example.com.pem
Automate that in a certbot deploy hook, or the certificate renews and HAProxy keeps serving the old one until someone notices. Our Let’s Encrypt guide covers deploy hooks.
Frequently Asked Questions
How is HAProxy different from nginx as a reverse proxy?
nginx is a web server that also proxies, so it serves static files and runs application handlers. HAProxy only proxies, and does it with better health checking, more balancing algorithms, richer statistics, and stronger TCP mode support.
What is the difference between HTTP mode and TCP mode?
HTTP mode parses requests, so it can route by hostname and path, rewrite headers, and health check with real HTTP requests. TCP mode forwards bytes without understanding them, which is what you need for databases, SSH, or any non-HTTP protocol.
Why does a backend keep going down and coming back?
Usually a health check that is stricter or more frequent than the backend can satisfy. A check hitting an expensive endpoint every two seconds will fail under load. Point checks at a cheap dedicated endpoint and give reasonable intervals.
What load balancing algorithm should I use?
roundrobin for uniform backends, leastconn when request durations vary widely such as with long-lived connections, and source when you need the same client to reach the same backend without shared session storage.
Do I need HAProxy for a single backend?
Not usually. With one backend, nginx or Caddy is simpler and gives you TLS and static file serving too. HAProxy earns its place when you have several backends, need real health checking, or are proxying something that is not HTTP.
Can HAProxy terminate TLS?
Yes, and it can also pass TLS through to the backend untouched, or re-encrypt on the way out. Terminating at the proxy is the common arrangement, and passthrough is what you want when the backend must see the original certificate.