$linuxjunkies
>

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.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

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:

  • globalprocess-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:

AlgorithmBest for
roundrobinEqual-capacity servers, stateless apps
leastconnLong-lived connections (WebSocket, SQL)
sourceSimple IP-based stickiness (less flexible than stick tables)
randomLarge 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.cfg and 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 1 or 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.
tested on:Ubuntu 24.04Debian 12Rocky 9Arch rolling

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