HAProxy: Load Balancing and Health Checking for Self-Hosted Services

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.