Cheat Sheet

HAProxy Config

HAProxy is the TCP and HTTP load balancer in front of many busy Linux sites. This sheet covers the config file itself, frontends, backends, ACLs, SSL, timeouts, and rate limiting, alongside the commands that check a config, reload it without dropping connections, and drive the stats socket.

Last updated September 11, 2026

Config File

One file, , read top to bottom and built out of named blocks.

BlockWhat it holds
Process settings: user, chroot, log target, stats socket, SSL defaults
Values inherited by every block below it: mode, timeouts, logging
The lines and the rules that pick a backend
The pool, the balance algorithm, health checks
A frontend and a backend fused into one block
DNS servers for resolving hostnames at runtime
Users and groups for
PathWhat it is
The main config on Debian, Ubuntu, and RHEL
Extra files, merged in filename order (2.4+)
Debian and Ubuntu: the and the unit passes
The RHEL, Rocky, and Alma equivalent
The packaged error pages points at
Where the official Docker image looks instead

There is no directive. Split a large config by passing a directory to (or by dropping files in ), which loads every in it in name order. A second block resets the inheritance for everything after it, which is how one file serves both HTTP and TCP sections.

Frontend, Backend & Listen

A frontend accepts traffic, a backend holds the server pool, and is both in one block. A minimal complete config:

global
    log /dev/log local0
    stats socket /run/haproxy/admin.sock mode 660 level admin

defaults
    mode http
    log global
    option httplog
    timeout connect 5s
    timeout client 30s
    timeout server 30s

frontend fe_web
    bind :80
    default_backend bk_web

backend bk_web
    balance roundrobin
    server web1 10.0.0.11:8080 check
    server web2 10.0.0.12:8080 check

The built-in stats page is the classic block:

listen stats
    bind :8404
    stats enable
    stats uri /stats
    stats refresh 10s
    stats auth admin:changeme

Add to enable/disable servers from the page itself; keep the port firewalled or bound to an internal address.

Check the Config

Always check before a reload; a config error found at reload time is a config error found the hard way.

CommandWhat it does
Parse the config, exit 0 when valid
Same, and print "Configuration file is valid"
Check every config file in a directory, loaded in name order

Chain the check with the reload so a typo never goes live:

haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy

Service Control

HAProxy runs under systemd on every mainstream distro; the systemctl cheat sheet covers the full service surface.

CommandWhat it does
Apply config changes without dropping connections
Full restart; drops every active connection
Start the service
Stop it
Start now and on every boot
Running or not, recent log lines, worker PIDs
Confirm which ports it is actually listening on

Reload is the one to remember: it starts new workers on the new config while the old workers finish their existing connections.

Stats Socket

Runtime admin without touching the config, also called the Runtime API. It needs one line in the section, then a reload:

global
    stats socket /run/haproxy/admin.sock mode 660 level admin

Send commands with socat, one-shot or interactive:

echo "show stat" | socat stdio /run/haproxy/admin.sock
socat readline /run/haproxy/admin.sock    # interactive; type "prompt" to keep it open
Socket commandWhat it does
Version, uptime, current and max connections
Full stats as CSV: every frontend, backend, and server
Every backend server with its state and health
Stop sending traffic to a server (maintenance)
Put it back in rotation
No new connections; existing ones finish
Back to normal after drain or maint
Change a server's load balancing weight
List live sessions
Kill every session on one server
Recent request and response parse errors
Stick table contents, e.g. rate limiting counters
Reset the stats counters
Change the global connection limit live
List every socket command this build supports

prints around 80 columns. This trims it to name, status, and weight:

echo "show stat" | socat stdio /run/haproxy/admin.sock | cut -d, -f1,2,18,19 | column -s, -t

Logs

HAProxy logs through syslog, not to a file of its own. On Debian and Ubuntu the packaged rsyslog rule writes traffic to .

CommandWhat it does
Follow live traffic logs
Process events only: starts, reloads, config warnings
Find requests that got a 503
Per-server report: errors and response times (halog ships with HAProxy)
Per-URL report from the same log

The config side that makes traffic logging work:

global
    log /dev/log local0

defaults
    log global
    mode http
    option httplog
    option dontlognull

is still where reload failures land; the journalctl cheat sheet covers filtering it.

Termination State

Every line ends with a four-character field, the session state at disconnection. The first character says who ended the session and the second says what it was doing, which is usually enough to tell a client problem from a backend problem.

CodeWhat happened
Normal completion, nothing to see
fired while waiting for response headers; the client got a 504
fired waiting for a free server slot; the client got a 503
The server refused or reset the connection; the client got a 502 or 503
The server closed mid-transfer
fired during the data phase
The client went away mid-transfer, which is normal for cancelled downloads
The client closed before sending a complete request
fired; headers never arrived in full
HAProxy rejected the request itself: a deny rule or a malformed request
HAProxy answered locally: a redirect, the stats page, an
HAProxy ran out of a resource (sockets, memory) while connecting

Lowercase first character means a timeout, uppercase means an abort or an explicit action. A run of points at being shorter than the app's slowest response.

Health Checks

on a server line turns on TCP health checks; upgrades them to real HTTP requests.

backend bk_web
    option httpchk
    http-check send meth GET uri /health hdr Host example.com
    http-check expect status 200
    server web1 10.0.0.11:8080 check inter 2s fall 3 rise 2
    server web2 10.0.0.12:8080 check inter 2s fall 3 rise 2
    server spare 10.0.0.13:8080 check backup
Server optionWhat it does
Enable health checks on this server
Time between checks
Failed checks before the server is marked down
Passed checks before it comes back up
Only used when every non-backup server is down
Cap concurrent connections to this server

Watch the results with on the stats socket, or on the stats page.

Command Line Options

CommandWhat it does
Version
Full build info: OpenSSL version, compiled features
Config file; repeat the flag or pass a directory
Check the config and exit
Run in the foreground with output to the terminal
Run as a background daemon
Master-worker mode; adds systemd notify (what the unit uses)
New process takes over; old one finishes its connections
New process takes over; old one is killed immediately
Inherit listening sockets from the old process (truly hitless reload)
Write the PID file

Balance

AlgorithmWhen to use it
The default: even spread, respects weights, adjustable at runtime
Long-lived connections: databases, LDAP, WebSocket
Hash of the client IP, so a client keeps hitting one server
Hash of the path; sends the same object to the same cache
Hash of a header value
Picks two servers at random and takes the less loaded one
Round robin with fixed weights, no runtime weight changes
Fill one server to its , then move to the next

Cookie-based sticky sessions, which survive a server coming back better than does:

backend bk_app
    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

re-hashes when a server goes down, which moves other clients too. The cookie only moves the clients on the dead server.

ACLs

ACLs name a condition; , , and friends act on it. Rules run top to bottom, first match wins.

frontend fe_web
    bind :80
    acl is_api path_beg /api
    acl is_static path_end .css .js .png .jpg
    acl host_blog hdr(host) -i blog.example.com
    acl internal src 10.0.0.0/8 192.168.0.0/16
    http-request deny if is_api !internal
    use_backend bk_api if is_api
    use_backend bk_blog if host_blog
    use_backend bk_static if is_static
    default_backend bk_web

Anonymous ACLs inline in braces work too, as in .

Headers

The backend sees HAProxy's IP, not the client's, until you say otherwise. belongs in so every backend inherits it.

DirectiveWhat it does
Add with the client IP
Same, but skip requests from a trusted source
Tell the app the client used TLS
Pass the port the client hit
Rewrite the Host sent to the backend
Add without replacing an existing header
Drop a header before it reaches the backend
Drop a header before it reaches the client
HSTS on every response
Pass the mTLS client certificate CN

is a fetch evaluated per request; quotes the result, which matters for anything that can contain spaces. The app still has to trust , and it should only do that when HAProxy is the only thing that can reach it.

HTTPS & SSL

HAProxy terminates TLS on the bind line. The file is the certificate, any intermediate chain, and the private key concatenated into one PEM, in that order.

frontend fe_https
    bind :443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
    http-request set-header X-Forwarded-Proto https
    default_backend bk_web

Building that PEM from a Let's Encrypt certificate:

sudo cat /etc/letsencrypt/live/example.com/fullchain.pem \
         /etc/letsencrypt/live/example.com/privkey.pem \
  | sudo tee /etc/haproxy/certs/example.com.pem > /dev/null

Point at a directory to load every certificate in it; HAProxy picks the right one per request by SNI. Verify the served chain with the SSL checker.

Redirects

frontend fe_web
    bind :80
    bind :443 ssl crt /etc/haproxy/certs/example.com.pem
    http-request redirect scheme https code 301 unless { ssl_fc }
    http-request redirect prefix https://example.com code 301 if { hdr(host) -i www.example.com }

The first rule sends every plain HTTP request to HTTPS; the second folds www onto the bare domain while keeping the path.

Timeouts & Limits

, , and have no defaults. Leave them out and HAProxy warns at startup, then lets stuck connections sit forever.

defaults
    timeout connect 5s
    timeout client 30s
    timeout server 30s
    timeout http-request 10s
    timeout http-keep-alive 10s
    timeout queue 30s
    timeout check 5s
    timeout tunnel 1h
SettingWhat it covers
Waiting for the TCP connection to a backend server
Client inactivity once the request is in flight
Backend inactivity while HAProxy waits for a response
Time the client gets to send complete headers (slowloris guard)
Idle time between requests on a kept-alive connection
Wait for a free server slot before the request gets a 503
Health check timeout once the check has connected
WebSocket and CONNECT tunnels, after the upgrade
Process-wide connection cap in
Per-server cap on a line; the rest queue
Kill workers left over from a reload after this long

Raise for WebSockets: after the upgrade, and no longer apply, so a short tunnel timeout is what cuts idle sockets.

Rate Limiting

A stick table counts what a key does; a rule reads the counter and acts.

frontend fe_web
    bind :80
    stick-table type ip size 100k expire 60s store http_req_rate(10s)
    http-request track-sc0 src
    http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
Socket commandWhat it does
Every key in the table with its counters
Only the keys over a threshold
Drop one entry, which unbans that IP
Empty the table
Reset one counter

Swap for to limit new connections, or for concurrent ones. For a block that outlives the request, track the source in a second table with a longer and deny on .

TCP Mode

proxies raw bytes: databases, SMTP, Redis, anything that is not HTTP. Frontend and backend must both be .

frontend fe_pg
    mode tcp
    bind :5432
    default_backend bk_pg

backend bk_pg
    mode tcp
    balance leastconn
    option tcp-check
    server pg1 10.0.0.21:5432 check
    server pg2 10.0.0.22:5432 check backup

beats for long-lived connections like database sessions.

SNI

To route TLS by hostname without terminating it (passthrough), read the SNI from the ClientHello in TCP mode:

frontend fe_tls_passthrough
    mode tcp
    bind :443
    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }
    use_backend bk_app1 if { req_ssl_sni -i app1.example.com }
    use_backend bk_app2 if { req_ssl_sni -i app2.example.com }

The backends stay and the apps keep their own certificates. When HAProxy terminates TLS itself, match on instead of .

Docker

The official image reads , not , and runs as the user. Docker's default capability set includes , so binding inside the container still works.

docker run -d --name hap -p 80:80 -p 443:443 \
  -v "$PWD/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro" \
  haproxy:3.0

# check a config without installing HAProxy at all
docker run --rm -v "$PWD/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro" \
  haproxy:3.0 haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
CommandWhat it does
Reload the config in place, no container restart
Build info and compiled features for that image
Follow the logs
Pick up a changed mount under Compose

There is no syslog socket in the container, so send logs to stdout instead:

global
    log stdout format raw local0 info

The docker cheat sheet covers the container side.

Install

CommandWhat it does
Debian and Ubuntu, usually a release or two behind
Ubuntu PPA with current builds, then
RHEL, Rocky, and Alma from AppStream
Alpine
Confirm which version you ended up with
Version plus the build date and OpenSSL it links

The distro package is fine for most sites; the PPA is for when you need a feature from a newer branch. The syntax used above needs 2.2 or newer, so check before copying config off the internet.

Keepalived

One HAProxy box is a single point of failure. Keepalived runs on two of them and moves a floating IP (VRRP) to whichever is alive, so clients only ever see one address.

vrrp_script chk_haproxy {
    script "killall -0 haproxy"
    interval 2
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 101
    virtual_ipaddress {
        203.0.113.50
    }
    track_script {
        chk_haproxy
    }
}

The backup node uses and a lower . Set in sysctl on both so HAProxy can bind the floating IP while it lives on the other box.

Gotchas

  • drops every active connection. After a config edit, is what you want, and only after passes.
  • Stats socket changes (, , ) vanish at the next reload. Anything permanent belongs in the config file.
  • The PEM must contain the private key. Certbot's alone fails at startup with "unable to load SSL private key".
  • No traffic logs usually means a missing or rsyslog rule; only ever shows process events. With enabled, the syslog socket must exist inside the chroot (the Debian package sets this up).
  • A 503 from HAProxy itself, with the backends supposedly fine, means no server in the chosen backend passed its health check. on the stats socket says why.
  • must match between a frontend and the backends it routes to; catches the mismatch, another reason to run it every time.
  • "cannot bind socket (permission denied)" on or means HAProxy is no longer root at bind time. The packaged unit binds as root and then drops to the user; a hardened unit or a container needs instead. The same message with a bind on a floating IP usually needs .
  • and the timeouts come from the nearest block above a section, not from the first one in the file. Add a second for the TCP sections and everything after it inherits that one, HTTP sections included.

HAProxy FAQ

Where is the HAProxy config file?

/etc/haproxy/haproxy.cfg on Debian, Ubuntu, RHEL, and their derivatives; the systemd unit picks it up through the CONFIG variable in /etc/default/haproxy or /etc/sysconfig/haproxy. Recent units also load /etc/haproxy/conf.d/ as a directory, so extra .cfg files there are merged in filename order and you can split per-site blocks out of the main file. The official Docker image is the odd one out at /usr/local/etc/haproxy/haproxy.cfg, and pfSense and OPNsense generate the file from their web UI, so edit those there rather than on disk.

Is HAProxy layer 4 or layer 7?

Both, and the mode line decides which. mode http parses requests, so ACLs can match paths and headers, cookies can pin sessions, and HAProxy can rewrite what it forwards; that is layer 7. mode tcp forwards bytes without reading them, which is what you want for Postgres, SMTP, Redis, or TLS passthrough; that is layer 4. The mode must match between a frontend and every backend it routes to. In TCP mode you can still route on the TLS SNI, because that field is readable before the handshake completes.

How do I check the HAProxy config before reloading?

Run haproxy -c -f /etc/haproxy/haproxy.cfg. It exits 0 when the config parses and prints each error with its line number when it does not; add -V for an explicit 'Configuration file is valid'. Chain it with the reload so a typo never takes the proxy down: haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy. A failed reload under systemd leaves the old process running, but the check gives you the error message instead of a silent unit failure.

How do I reload HAProxy without dropping connections?

sudo systemctl reload haproxy. Modern packages run HAProxy in master-worker mode, so a reload starts new workers on the new config while the old workers finish their existing connections and then exit. systemctl restart is different: it stops the process first and drops every active connection. Long-lived connections such as WebSockets can keep an old worker alive for a long time; cap that with hard-stop-after 30s in the global section.

Where does HAProxy write its logs?

To syslog, not to a file of its own. The config needs log /dev/log local0 in the global section and log global with option httplog in defaults; the Debian and Ubuntu packages ship an rsyslog rule that then writes traffic to /var/log/haproxy.log. journalctl -u haproxy shows only process events such as starts, reloads, and config warnings. If the log file stays empty, the rsyslog rule is missing or HAProxy is chrooted without a syslog socket inside the chroot.

Related cheat sheets