How to Set Up HAProxy as a Load Balancer
Set up HAProxy as an HTTP load balancer with active health checks, stick tables for session persistence, and a live stats dashboard — step by step.
Before you start
- ▸Two or more backend application servers reachable from the HAProxy host
- ▸A health-check HTTP endpoint on each backend that returns 200 when healthy
- ▸Root or sudo access on the HAProxy server
- ▸Basic familiarity with systemd service management
HAProxy is one of the most battle-tested load balancers available on Linux. It handles millions of concurrent connections, offers fine-grained health checking, and gives you real-time traffic visibility through its built-in stats page. This guide walks through a complete, working HAProxy setup: a frontend that accepts client traffic, backends with health checks, session persistence via stick tables, and the stats dashboard.
Install HAProxy
The package is available in every major distro's repositories. Version 2.6 or later is recommended for current deployments.
Debian / Ubuntu
sudo apt update && sudo apt install -y haproxy
Fedora / RHEL 9 / Rocky Linux
sudo dnf install -y haproxy
Arch Linux
sudo pacman -S haproxy
Check the installed version before proceeding:
haproxy -v
Output will look something like HAProxy version 2.8.x. Anything below 2.4 lacks some stick-table improvements shown later.
Core Configuration Concepts
HAProxy's configuration lives in /etc/haproxy/haproxy.cfg. It is split into four sections:
- global — process-level settings (logging, user, limits).
- defaults — values inherited by all frontends and backends unless overridden.
- frontend — what HAProxy listens on and how it routes traffic.
- backend — the pool of servers that actually serve requests.
A fifth section, listen, combines a frontend and backend into one block and is convenient for the stats page.
Write the Configuration
Back up the default file, then replace it entirely. The example below load-balances HTTP across three application servers.
sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.bak
sudo tee /etc/haproxy/haproxy.cfg > /dev/null <<'EOF'
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
maxconn 50000
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
option http-server-close
timeout connect 5s
timeout client 30s
timeout server 30s
retries 3
errorfile 400 /etc/haproxy/errors/400.http
errorfile 503 /etc/haproxy/errors/503.http
frontend http_front
bind *:80
default_backend app_servers
backend app_servers
balance roundrobin
option httpchk GET /health HTTP/1.1\r\nHost:\ app.example.com
http-check expect status 200
default-server inter 5s fall 3 rise 2
server app1 192.168.1.10:8080 check
server app2 192.168.1.11:8080 check
server app3 192.168.1.12:8080 check
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 5s
stats auth admin:changeme
stats show-legends
stats show-node
EOF
Adjust the server IPs, port, and health-check path to match your environment. The auth line sets HTTP Basic credentials for the stats page — use a strong password or restrict access by IP in production.
Health Checks Explained
The check keyword on each server line activates active health probing. The parameters set in default-server control behaviour:
- inter 5s — probe every 5 seconds.
- fall 3 — mark the server DOWN after 3 consecutive failures.
- rise 2 — mark it back UP after 2 consecutive successes.
option httpchk sends a real HTTP request rather than a bare TCP connect, so your backend can return 200 only when the application is genuinely ready. The http-check expect status 200 line makes HAProxy treat any other status code as a failure. For TCP-mode backends, drop both httpchk lines and HAProxy will use a TCP handshake check instead.
Load-Balancing Algorithms
The balance directive controls how connections are distributed. Common options:
| Algorithm | Best for |
|---|---|
roundrobin | Equal-capacity servers, stateless apps |
leastconn | Long-lived connections (WebSocket, SQL) |
source | Simple IP-based stickiness (less flexible than stick tables) |
random | Large backend pools where roundrobin causes thundering-herd |
Switch the algorithm by changing the single balance line inside the backend block.
Session Persistence with Stick Tables
For applications that store session state in memory (legacy PHP apps, certain Java stacks), you need the same client to always hit the same server. HAProxy's stick tables are more reliable than cookie-based or source-IP methods alone because they survive across backend connection resets.
Add the following to the backend app_servers block, after the balance line:
stick-table type ip size 200k expire 30m
stick on src
This creates an in-memory table keyed by client IP, stores up to 200,000 entries, and expires each entry after 30 minutes of inactivity. stick on src records which backend server served a client and routes future requests from the same IP to the same server.
For cookie-based stickiness (more accurate when clients share IPs behind NAT), use:
cookie SERVERID insert indirect nocache
server app1 192.168.1.10:8080 check cookie app1
server app2 192.168.1.11:8080 check cookie app2
server app3 192.168.1.12:8080 check cookie app3
HAProxy injects a SERVERID cookie on the first response. Subsequent requests from the same browser carry that cookie, and HAProxy routes them accordingly — no stick-table lookup needed.
Enable and Start HAProxy
sudo systemctl enable --now haproxy
On configuration changes, validate before reloading to avoid downtime:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
A clean config prints Configuration file is valid. Then do a zero-downtime reload:
sudo systemctl reload haproxy
reload (not restart) tells HAProxy to fork a new process, transfer listening sockets, drain the old process, and exit it — existing connections are not dropped.
Open Firewall Ports
firewalld (Fedora / RHEL / Rocky)
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=8404/tcp
sudo firewall-cmd --reload
ufw (Debian / Ubuntu)
sudo ufw allow 80/tcp
sudo ufw allow 8404/tcp
In production, restrict port 8404 to a management subnet rather than allowing it publicly.
Verify the Setup
Check the service is running and there are no errors in the journal:
sudo systemctl status haproxy
sudo journalctl -u haproxy -n 50 --no-pager
Send a test request through the load balancer:
curl -I http://<haproxy-ip>/health
Open the stats page in a browser at http://<haproxy-ip>:8404/stats. Each backend server shows its current state (green = UP, red = DOWN), request rate, session count, and health check results. You can also query via the socket:
echo 'show info' | sudo socat stdio /run/haproxy/admin.sock
Troubleshooting
- 503 Service Unavailable — all backend servers are marked DOWN. Check that your health-check endpoint (
/health) returns HTTP 200 and is reachable from the HAProxy host directly:curl http://192.168.1.10:8080/health. - Config validation fails — run
haproxy -c -f /etc/haproxy/haproxy.cfgand read the line number in the error. Common culprits: wrong indentation (HAProxy requires spaces, not tabs, in some older versions), missing newline at end of file, or an IP typo. - Stats page unreachable — confirm the firewall allows port 8404 and that HAProxy bound to the port (
ss -tlnp | grep haproxy). - SELinux blocking connections (RHEL/Rocky) — HAProxy may be denied from connecting to non-standard ports. Run
sudo setsebool -P haproxy_connect_any 1or write a targeted policy. - Stick table not persisting across restarts — stick tables are in-memory by default. HAProxy Enterprise supports peer synchronisation; the community version loses table state on restart, so design your app to handle occasional session loss or use cookie-based stickiness instead.
Frequently asked questions
- What is the difference between a stick table and cookie-based stickiness?
- A stick table is server-side: HAProxy stores which backend served each client IP in memory. Cookie stickiness is client-side: HAProxy sets a cookie in the browser, which returns it on every request. Cookie stickiness is more accurate when many clients share one IP (office NAT), but requires the client to accept cookies.
- How do I add or remove a backend server without dropping live traffic?
- Edit haproxy.cfg, then run sudo haproxy -c -f /etc/haproxy/haproxy.cfg to validate, followed by sudo systemctl reload haproxy. The reload forks a new process and drains the old one gracefully — no connections are interrupted.
- Can HAProxy handle HTTPS / TLS termination?
- Yes. Add ssl crt /etc/ssl/private/cert.pem to the bind line in your frontend and change the port to 443. HAProxy decrypts traffic and forwards plain HTTP to backends, keeping TLS processing centralised.
- Why are all my backend servers showing DOWN immediately after startup?
- The most common causes are: the health-check path does not return HTTP 200, a firewall on the backend hosts blocks connections from HAProxy, or the servers are not yet listening when HAProxy starts. Use curl directly from the HAProxy host to the backend IP and port to isolate the issue.
- Is HAProxy's stats page safe to expose publicly?
- Not without additional controls. Even with HTTP Basic Auth, it reveals internal IP addresses and traffic patterns. Restrict access to a management VLAN or VPN using an ACL in the listen stats block, for example: acl mgmt src 10.0.0.0/8 and http-request deny unless mgmt.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.