The HAProxy peer protocol replicates stick-table contents across multiple HAProxy instances. Without it, a cluster of load balancers has N independent rate-limit counters, sticky session tables, and gpc counters — and any reload wipes them. With it, the state is shared, survives reloads on any one node, and survives the loss of a node. This article covers the config, the network requirements, the reload handover, and the failure modes we plan for.
How to verify
For an existing peer setup, the runtime API tells you which peers are connected and synced:
echo "show peers" | sudo socat /run/haproxy/admin.sock -
echo "show table" | sudo socat /run/haproxy/admin.sock -
ss -ltnp | grep :1024
sudo journalctl -u haproxy -n 100 --no-pager | grep -i peer
show peers lists each peer in the section, its connection status, and the last successful sync timestamp. If a peer is Down, the journal will say why — usually network unreachable, TLS rejected, or hostname mismatch.
What’s happening
A peers section names a group of HAProxy instances that replicate stick-tables. Each member declares its peer name and listen address. When two peers can reach each other, they open a TCP connection on the peer port and stream updates: new keys, counter increments, expirations.
The protocol is push-only — each peer pushes its own table changes to all other peers. There is no leader. New entries arrive at peers in roughly real-time (sub-second for normal traffic).
Two important properties:
- The local peer name must match the system’s resolvable hostname or be
localpeer. HAProxy needs to identify “which entry in the peers section is me” — it does this by matching thepeerline’s name to the hostname. Thelocalpeerkeyword overrides that. - The peer port is not encrypted by default. Use the modern
bindsyntax inside the peers section withsslto require TLS, or rely on a private VLAN.
On reload, the new master inherits the peer connections from the old worker via FD passing. There is no resync storm.
When a peer crashes and comes back, it requests the full table from any reachable peer; once synced, normal incremental replication resumes.
The procedure
-
Basic three-node peer group. Same
peersblock on every node, with each node’s hostname appearing as apeer:peers haproxy-cluster peer hap1 10.0.10.5:1024 peer hap2 10.0.10.6:1024 peer hap3 10.0.10.7:1024On hap1, the
peer hap1line is the local listener; the other two are remotes. The hostnamehap1(fromhostname -s) must match the peer name. -
Reference the peer group from stick-tables. Add
peers <name>to each table you want to replicate:backend be_app stick-table type ip size 200k expire 1h store http_req_rate(10s),conn_rate(10s) peers haproxy-cluster http-request track-sc0 srcEvery node uses the same
backendandstick-tabledefinition. The peer protocol does the syncing. -
Open the firewall. Port 1024 between peers, plus a UFW or security group rule:
sudo ufw allow from 10.0.10.0/24 to any port 1024 proto tcp sudo ufw status verboseThe peer port is bidirectional — each pair of peers opens one TCP connection between them; either side can initiate.
-
Verify connectivity on bring-up. From each node, confirm it sees the others:
echo "show peers" | sudo socat /run/haproxy/admin.sock -Output should show
LOCALfor the current node andUpfor each remote, with a recentlast_status_changetimestamp. -
Add TLS for the peer link. When the peer link traverses a network you don’t fully trust, wrap it in TLS:
peers haproxy-cluster bind 10.0.10.5:1024 ssl crt /etc/haproxy/certs/hap1-peer.pem ca-file /etc/haproxy/ca/peer-ca.pem verify required server hap1 server hap2 10.0.10.6:1024 ssl ca-file /etc/haproxy/ca/peer-ca.pem verify required sni str(hap2) server hap3 10.0.10.7:1024 ssl ca-file /etc/haproxy/ca/peer-ca.pem verify required sni str(hap3)The
serversyntax (instead ofpeer) is the newer form that exposes per-peer SSL options. Mix carefully — for plain text, the olderpeersyntax is shorter. -
Handle the resync window. On node restart, the joining peer requests a full table from a remote. During this window, ACLs that read the table see stale or empty data. Set
resync-timeoutand accept that rate limits may be briefly looser:peers haproxy-cluster peer hap1 10.0.10.5:1024 peer hap2 10.0.10.6:1024 resync-timeout 30s -
Plan for split-brain. If a network partition separates two peers, both keep counting. When they reconnect, the protocol merges — the peer with the higher count wins per entry. Rate limit counters double-count briefly during the merge; for sticky sessions, the more recent server-id wins. Document the behaviour; do not pretend it does not happen.
Common pitfalls
- The local hostname must match the
peername. Ifhostname -sreturnshap-prod-01.internalbut the peers section sayspeer hap1 ..., HAProxy logslocal peer name 'hap1' not found in peers section. Either fix the hostname or uselocalpeer hap1in the global section. - Without
peers, every reload wipes the table. We have seen this surface as “rate limits don’t seem to be working” — the engineer reloaded HAProxy ten minutes earlier and the table is starting fresh. - The default peer port is unencrypted. On a flat shared VLAN with other tenants, this leaks counters. Use a dedicated management VLAN or wrap with TLS.
- Peers behind NAT need consistent reachable addresses. Two peers in different VPCs joined by a NAT gateway can talk to each other, but the address each sees depends on the NAT — get this right or peers loop trying to connect.
- The peer protocol replicates table data, not server state, not config.
disable servervia runtime API on one node does not propagate. For per-node operator commands, run them on every node.
Stack Harbor wires peers into every multi-instance HAProxy fleet — TLS-wrapped peer link on a management VLAN, identical config on every node, and a sync verification step in the deploy script. For multi-region clients, peers across regions add latency to replication; we deploy per-region peer groups and accept that rate limits are per-region. This is how we run Multi-region Environments where the load balancer fleet itself spans multiple sites.