Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

HAProxy: load balancing and high availability

By updated

Improve this page

haproxy logo

This guide lets you deploy HAProxy to spread HTTP traffic across several backend servers. You will configure round-robin load balancing, automatic health checks, sticky sessions and protection against abuse. At the end, you will have a reverse proxy ready for production.

Audience: system administrators, DevOps engineers, developers who want to improve the availability of their applications.

Prerequisites: a Linux server (Debian 13, Ubuntu 24.04 or RHEL 10) with root access, and at least two backend servers (or Docker containers).

What you will learn

  • Build HAProxy 3.2 from a verified archive and run it under systemd.
  • Spread traffic across several backends and remove a failing server automatically.
  • Route requests by path or header with ACLs.
  • Harden the entry point: TLS, security headers, rate limiting, blocklist.
  • Drive the service live through the administration socket, without a reload.

What is HAProxy?

HAProxy (High Availability Proxy) is an open source load balancer and reverse proxy. Created in 2000 by Willy Tarreau, it is used at large scale in production to handle millions of simultaneous connections.

Why use HAProxy?

Without HAProxyWith HAProxy
A single server receives all the trafficTraffic is spread across several servers
If the server goes down, the site is unavailableTraffic fails over automatically to the healthy servers
No protection against load spikesQueue management and rate limiting
TLS configured on every serverCentralised TLS termination

Prerequisites

Installing HAProxy 3.2

This guide uses HAProxy 3.2.25, the latest release of the 3.2 branch as of 30 September 2026. The choice makes sense twice over on the security side: the 3.2 branch is LTS (security fixes until ~2030) and it is the one that went through the security audit commissioned by ANSSI (on 3.2.5). You install the latest 3.2.x to combine the branch audit with all its fixes.

For a secure installation, build from the official sources downloaded from haproxy.org:

  1. Install the build dependencies:

    Terminal window
    sudo apt install -y build-essential libssl-dev libpcre2-dev zlib1g-dev liblua5.4-dev
  2. Download and extract the sources:

    Terminal window
    cd /tmp
    curl -LO https://www.haproxy.org/download/3.2/src/haproxy-3.2.25.tar.gz
    curl -LO https://www.haproxy.org/download/3.2/src/haproxy-3.2.25.tar.gz.sha256
    sha256sum -c haproxy-3.2.25.tar.gz.sha256
    tar xzf haproxy-3.2.25.tar.gz
    cd haproxy-3.2.25

    The .sha256 file published next to the archive lets you verify it has not been tampered with: sha256sum -c must answer haproxy-3.2.25.tar.gz: OK before any build.

  3. Build with the recommended options:

    Terminal window
    make -j$(nproc) TARGET=linux-glibc USE_OPENSSL=1 USE_PCRE2=1 USE_ZLIB=1 USE_LUA=1 USE_PROMEX=1

    Build options:

    • USE_OPENSSL=1: TLS support
    • USE_PCRE2=1: fast regular expressions
    • USE_ZLIB=1: HTTP compression
    • USE_LUA=1: Lua scripting
    • USE_PROMEX=1: built-in Prometheus exporter
  4. Install:

    Terminal window
    sudo make install PREFIX=/usr/local
  5. Check the installation:

    Terminal window
    /usr/local/sbin/haproxy -v

    You should see:

    HAProxy version 3.2.25-70469d3 2026/09/28 - https://haproxy.org/
    Status: long-term supported branch - will stop receiving fixes around Q2 2030.

HAProxy key concepts

Before configuring HAProxy, let us understand the building blocks of its architecture.

Frontend, Backend and Listen

HAProxy uses three types of configuration blocks:

BlockRole
frontendEntry point that receives client connections
backendGroup of servers that handle the requests
listenCombines frontend + backend in a single block

The diagram follows the path of a request: the frontend receives it, then the backend picks a server according to the declared load-balancing algorithm.

Clients send their HTTP requests to the HAProxy frontend, which passes them to the backend; the backend spreads them across Web 1, Web 2 and Web N with the roundrobin, leastconn or source algorithm.HTTP requestsClientsHAPROXYfrontendBACKENDroundrobin · leastconn · sourceWeb 1Web 2Web N
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

Load-balancing algorithms

HAProxy offers several methods to distribute traffic:

AlgorithmDescriptionUse case
roundrobinHands out requests in turnIdentical servers, balanced load
leastconnSends to the server with the fewest connectionsLong requests (downloads, WebSocket)
sourceHash of the source IP (sticky)Session affinity without a cookie
uriHash of the URIDistributed cache
randomRandom selectionSimple and effective at large scale

ACLs (Access Control Lists)

ACLs let you take routing decisions based on conditions:

acl is_api path_beg /api # True if the path starts with /api
acl is_admin hdr(host) -i admin.example.com # True if Host = admin...
use_backend api_servers if is_api
use_backend admin_servers if is_admin
default_backend web_servers

Common condition types:

ConditionDescription
path_beg /apiThe path starts with "/api"
path_end .jpgThe path ends with ".jpg"
hdr(host) -i example.comThe Host header is "example.com"
src 192.168.1.0/24The source IP is in that subnet

Health Checks

Health checks automatically verify that your backend servers work:

  • TCP check: checks that the port answers (default)
  • HTTP check: sends an HTTP request and expects a 200 code
  • If a server does not answer, HAProxy removes it automatically from the pool

Basic configuration

Now that you understand the concepts, let us move to practice. The main configuration file is /etc/haproxy/haproxy.cfg. The configuration below is a working starting point you can copy, then adapt to your infrastructure.

What this configuration does:

  • Listens on port 80 (HTTP)
  • Spreads traffic across two web servers (192.168.1.10 and 192.168.1.11)
  • Automatically checks that the servers answer (health checks)
  • Provides a secured statistics page on port 8404
/etc/haproxy/haproxy.cfg
global
log /dev/log local0
log /dev/log local1 notice
maxconn 4096
user haproxy
group haproxy
daemon
defaults
mode http
log global
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
timeout http-request 10s
# Statistics page (secured)
listen stats
bind 127.0.0.1:8404
stats enable
stats uri /stats
stats refresh 10s
stats show-legends
stats auth admin:CHANGE_THIS_PASSWORD
# HTTP frontend
frontend http_front
bind *:80
default_backend web_backend
# Backend with load balancing
backend web_backend
balance roundrobin
option httpchk
http-check send meth GET uri /health
http-check expect status 200
server web1 192.168.1.10:80 check
server web2 192.168.1.11:80 check

The file reads in four sections, always in this order, and knowing where a directive belongs avoids half of the configuration mistakes:

  1. global: settings of the HAProxy process (logs, user, limits)
  2. defaults: default values for frontends/backends
  3. frontend/listen: traffic entry points
  4. backend: destination server groups

Validation and startup

Before applying a configuration, always validate it:

Terminal window
# Checks the syntax
sudo /usr/local/sbin/haproxy -c -f /etc/haproxy/haproxy.cfg

If no error is printed, the configuration is valid. The full path of the binary is not a fad: on AlmaLinux 10, the secure_path of sudo does not contain /usr/local/sbin, and sudo haproxy answers command not found there; Debian 13 includes it.

Terminal window
# Start the service
sudo systemctl enable haproxy
sudo systemctl start haproxy
# Check the status
sudo systemctl status haproxy

Verification

Test that load balancing works:

Terminal window
# Send several requests
for i in {1..4}; do curl -s http://localhost/ | head -1; done

You should see the answers alternate between your backends.

Reach the statistics page, which only listens on the loopback, through an SSH tunnel: ssh -L 8404:localhost:8404 user@server, then http://localhost:8404/stats.

Advanced use cases

The basic configuration works for most static sites. But as soon as your application gets more complex, you will need advanced features. Let us look at the most common cases.

Sticky sessions (persistent sessions)

The problem: imagine an e-commerce site where the user adds items to the cart. If the first request goes to server A (which stores the cart), but the second goes to server B... the cart looks empty! Frustrating for the user.

The solution: sticky sessions guarantee that a user is always sent back to the same backend server. HAProxy uses a cookie to "remember" the server assigned to each visitor.

Terminal window
backend web_backend
balance roundrobin
cookie SERVERID insert indirect nocache
server web1 192.168.1.10:80 check cookie web1
server web2 192.168.1.11:80 check cookie web2

How it works:

  1. On the first connection, HAProxy picks a server (e.g. web1)
  2. It adds a cookie SERVERID=web1 to the response
  3. The following requests carrying this cookie always go to web1

Verification:

Terminal window
# First request: fetch the cookie
curl -c cookies.txt http://localhost/
# Following requests: use the cookie (same server)
curl -b cookies.txt http://localhost/
curl -b cookies.txt http://localhost/

URL-based routing (path-based routing)

The problem: your application has several components, an API, static files, WebSockets, each on different servers. How do you direct each type of request to the right server group?

The solution: use ACLs (filtering rules) to analyse the URL and route intelligently. Think of a railway switch: depending on the destination written on the ticket (the URL), the train (the request) is sent down the right track.

Terminal window
frontend http_front
bind *:80
# ACL definitions
acl is_api path_beg /api
acl is_static path_beg /static /images /css /js
acl is_websocket hdr(Upgrade) -i WebSocket
# Host validation (allow-list)
acl valid_host hdr(host) -i example.com www.example.com api.example.com
http-request deny deny_status 400 unless valid_host
# Routing to the right backends
use_backend api_backend if is_api
use_backend static_backend if is_static
use_backend websocket_backend if is_websocket
default_backend web_backend

Custom HTTP health checks

The problem: a server can be "up" (the process runs) but "sick" (the database is disconnected, the disk is full...). A simple TCP check does not detect these situations.

The solution: configure HTTP health checks that call a /health endpoint of your application. This endpoint checks that everything works (DB, cache, disk space...) and returns a 200 code if OK. HAProxy automatically removes the servers that do not answer correctly.

Terminal window
backend web_backend
balance roundrobin
# Enable HTTP health checks (HAProxy 3.x syntax)
option httpchk
http-check send meth GET uri /health ver HTTP/1.1 hdr Host localhost
# Expect a 200 code
http-check expect status 200
# Or check the content of the response
# http-check expect string "status":"healthy"
# Check parameters
server web1 192.168.1.10:80 check inter 5s fall 3 rise 2
server web2 192.168.1.11:80 check inter 5s fall 3 rise 2

Server parameters:

ParameterDescription
checkEnables health checks
inter 5sInterval between checks (5 seconds)
fall 3Number of failures before the server is considered DOWN
rise 2Number of successes to bring the server back UP

SSL/TLS termination

The problem: every backend server must handle its own TLS certificate, which complicates renewal and costs CPU for encryption.

The solution: HAProxy becomes the single TLS termination point. It decrypts incoming HTTPS requests, then forwards them as plain HTTP (or HTTPS) to your backends. Benefits:

  • A single certificate to manage
  • Lighter backends
  • Centralised renewal (Let's Encrypt, etc.)
Terminal window
frontend https_front
bind *:443 ssl crt /etc/haproxy/certs/site.pem alpn h2,http/1.1
bind *:80
# Redirect HTTP to HTTPS
http-request redirect scheme https unless { ssl_fc }
# Add the X-Forwarded-Proto header
http-request set-header X-Forwarded-Proto https if { ssl_fc }
default_backend web_backend

Why harden TLS? Protocols and cipher algorithms evolve. What was secure in 2015 (TLS 1.0, SHA-1) is vulnerable today. The configuration below applies the 2026 good practices:

  • TLS 1.2 minimum: disables the obsolete versions (1.0, 1.1)
  • Modern suites: only AES-GCM and ChaCha20 (fast and secure)
  • Safe elliptic curves: X25519, P-256, P-384

Add these settings to the global section of your configuration:

Terminal window
global
# TLS: minimum version and modern suites
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
# Safe elliptic curves
ssl-default-bind-curves X25519:P-256:P-384
# Same settings on the server side (if re-encrypting to HTTPS backends)
ssl-default-server-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256

If you must support old clients (IE11, Android 4.x), keep TLSv1.2 as the minimum. For 100% modern environments, you can move to ssl-min-ver TLSv1.3.

Securing HAProxy

HAProxy stands on the front line facing the Internet. It is both a risk (single entry point) and an opportunity (central control point). Let us see how to secure it.

Rate limiting

The problem: an attacker or a bot can send thousands of requests per second, saturating your servers and making the site unreachable for real users.

The solution: rate limiting counts the requests of each IP and blocks those that exceed a reasonable threshold. Think of a nightclub bouncer who turns away people who try to get in too often.

Beware: limits that are too strict can block legitimate users (several people behind the same corporate NAT). Start with conservative values and adjust according to your logs.

Terminal window
frontend http_front
bind *:80
# Tracking table of requests per IP
stick-table type ip size 100k expire 30s store http_req_rate(10s)
# Count the requests per IP
http-request track-sc0 src
# Block above 50 requests in 10 seconds
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 50 }
default_backend web_backend

Explanation:

  • stick-table: creates a table to store the counters per IP
  • track-sc0: ties each request to its source IP
  • deny_status 429: returns "Too Many Requests" when the limit is exceeded

Security headers

The problem: modern browsers offer protections against clickjacking, XSS, MIME sniffing... but these protections only kick in if the server sends the right HTTP headers.

The solution: configure HAProxy to add these security headers automatically to every response. It is a quick security win, without changing your applications.

What each header does:

HeaderProtection
Strict-Transport-SecurityForces HTTPS for 1 year (prevents HTTP downgrade)
X-Frame-OptionsPrevents embedding your site in an iframe (anti-clickjacking)
X-Content-Type-OptionsPrevents the browser from "guessing" the MIME type
Content-Security-PolicyControls which resources may be loaded
Terminal window
frontend https_front
bind *:443 ssl crt /etc/haproxy/certs/site.pem
# Security headers
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
http-response set-header X-Frame-Options "SAMEORIGIN"
http-response set-header X-Content-Type-Options "nosniff"
http-response set-header Referrer-Policy "strict-origin-when-cross-origin"
http-response set-header Permissions-Policy "geolocation=(), microphone=(), camera=()"
# Basic CSP - adapt to your application
http-response set-header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'self'"
default_backend web_backend

Block malicious IPs

The problem: you spot IPs attacking your site (brute force, abusive scraping, spam). You want to block them before they reach your backends.

The solution: create a blocklist of IPs in an external file. HAProxy loads this file at startup and rejects the matching requests. Recorded on HAProxy 3.2.25: an address added to the file is not blocked until the service is reloaded. To block right away, without a reload, go through the administration socket, then copy the address into the file so that it survives the next reload:

Terminal window
frontend http_front
bind *:80
# IP blocklist (external file)
acl blocked_ips src -f /etc/haproxy/blocked_ips.txt
http-request deny if blocked_ips
default_backend web_backend

Content of /etc/haproxy/blocked_ips.txt:

192.168.1.100
10.0.0.0/8
Terminal window
echo "add acl /etc/haproxy/blocked_ips.txt 203.0.113.7" | sudo socat stdio /run/haproxy/admin.sock

Check that the protections work

Hardening is only worth something once tested. Here is a frontend that combines security headers, rate limiting, blocking of a sensitive path and a statistics page protected by authentication, all in front of two backends:

Terminal window
frontend web
bind *:80
# Security headers on every response
http-response set-header X-Frame-Options DENY
http-response set-header X-Content-Type-Options nosniff
# Rate limiting: 20 requests / 10 s per IP, otherwise 429
stick-table type ip size 100k expire 30s store http_req_rate(10s)
http-request track-sc0 src
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 20 }
# Block a sensitive path
acl admin_path path_beg /admin
http-request deny if admin_path
default_backend servers
frontend stats
bind 127.0.0.1:8404
stats enable
stats uri /stats
stats auth admin:a_strong_password

Each protection is checked with curl. The headers are added to every response:

Terminal window
curl -sI http://localhost/ | grep -i "x-"
x-frame-options: DENY
x-content-type-options: nosniff

The ACL blocks /admin with a 403, without ever reaching the backend. Rate limiting lets the first requests through then returns 429 (Too Many Requests) above the threshold, and the stats page requires authentication:

GET /admin -> 403
25 rapid requests -> 200 19 times, then 429 429 429 429 429 429
GET /stats no auth -> 401
GET /stats with auth -> 200

These four checks, replayed on 30 September 2026 on HAProxy 3.2.25, from the server itself, turn a simple load balancer into a security control point at the entrance of the infrastructure. That is the whole point of centralising protection on the reverse proxy rather than scattering it across every backend.

Monitoring and logs

An infrastructure without monitoring is like driving at night without headlights. HAProxy offers several tools to see what happens in real time and diagnose problems.

Statistics page

What you see: the built-in stats page is a visual dashboard showing the state of your infrastructure at a glance. Each backend is shown with a colour code (green = UP, red = DOWN, orange = in transition).

Available information:

  • Number of active and total connections
  • Request rate per second
  • Average backend response time
  • State of each server (UP/DOWN/DRAIN)
  • Error history
Terminal window
listen stats
bind 127.0.0.1:8404
stats enable
stats uri /stats
stats refresh 10s
stats show-legends
stats show-node
stats auth admin:SECURE_PASSWORD

Reach http://localhost:8404/stats through an SSH tunnel to see:

  • The state of each backend (UP/DOWN)
  • The number of active connections
  • The request rates
  • The response times

JSON logs

The problem: traditional text logs are hard to parse and analyse at scale. Every tool (grep, awk, Elasticsearch) has to "understand" the format.

The solution: HAProxy can format logs directly as JSON (format accepted by 3.2.25). The format is only applied if the frontend has a log destination: without log global, haproxy -c warns log format ignored. Each field is clearly identified, which eases ingestion into tools like Elasticsearch, Loki or Datadog.

Terminal window
defaults
mode http
log-format "%{+json}o %(client_ip)ci %(status)ST %(bytes)B %(duration)Ta %(backend)b %(server)s"

Prometheus integration

Why Prometheus? It is the de facto standard for monitoring in Kubernetes/cloud-native environments. HAProxy can expose its metrics in Prometheus format, which lets you build Grafana dashboards and automatic alerts.

What you can monitor: active connections, requests per second, average latency, 5xx errors, backend state...

Terminal window
frontend prometheus
bind 10.0.0.5:8405
http-request use-service prometheus-exporter if { path /metrics }
no log

Bind this frontend to the internal address the Prometheus server reaches (here 10.0.0.5, to adapt), never to *: the metrics reveal the topology of the backends. Do not add stats enable to it, which would publish the statistics page without authentication. Then configure Prometheus to scrape http://10.0.0.5:8405/metrics.

Troubleshooting

Even with a careful configuration, problems can occur. This section helps you diagnose and solve the most frequent situations.

Common problems

How to read this table: identify the symptom you observe, then apply the matching solution.

SymptomLikely causeSolution
"bind: cannot bind socket"Port already in useCheck with ss -tlnp | grep 80
cannot bind socket (Permission denied) for [0.0.0.0:80]HAProxy launched under a non-root accountStart the service as root, let user/group in global drop the privileges
Unknown command: 'reload' on the socketCommand sent to admin.sockUse the master socket (-S) or systemctl reload haproxy
sudo haproxy: command not found (RHEL)/usr/local/sbin missing from secure_pathCall /usr/local/sbin/haproxy by its full path
Server DOWN in the statsHealth check failsCheck that the /health endpoint answers 200
No load balancingOnly one server UPCheck the health checks of the other servers
503 errorEvery server DOWNCheck network connectivity to the backends
Session cookie keeps changingWrong cookie configurationCheck the cookie parameter on each server

Runbook commands (daily operations)

Keep this section at hand: these are the commands you will use most often in production. Beware of the reload command: the administration socket (admin.sock) refuses it with Unknown command: 'reload', it only exists on the master socket, opened by the -S option at launch.

Terminal window
# Full configuration validation (with warnings)
sudo /usr/local/sbin/haproxy -c -V -f /etc/haproxy/haproxy.cfg
# Reload without interruption (hitless reload with seamless)
sudo systemctl reload haproxy
# Manual reload through the master socket (HAProxy launched with -S /run/haproxy/master.sock)
echo "reload" | sudo socat stdio /run/haproxy/master.sock
# Follow the logs in real time
sudo journalctl -u haproxy -f
# Check the listening ports
ss -tlnp | grep haproxy
# Test a request in verbose mode
curl -v http://localhost/

Administration socket (runtime API)

The problem: reloading HAProxy for every small change (server maintenance, debug) can be heavy and risky in production.

The solution: the administration socket lets you interact with HAProxy in real time, without touching the configuration or reloading the service. It is like having a hidden administration console.

Terminal window
# Enable the socket in the global section
global
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
# Usage examples
# See the state of the backends
echo "show stat" | sudo socat stdio /run/haproxy/admin.sock
# Temporarily disable a server (maintenance)
echo "disable server web_backend/web1" | sudo socat stdio /run/haproxy/admin.sock
# Re-enable the server
echo "enable server web_backend/web1" | sudo socat stdio /run/haproxy/admin.sock
# See the active sessions
echo "show sess" | sudo socat stdio /run/haproxy/admin.sock

Diagnostic mode

When to use it: your configuration is valid (haproxy -c passes) but something does not behave as expected. Diagnostic mode prints extra warnings about suspicious or obsolete directives.

Terminal window
sudo /usr/local/sbin/haproxy -dD -c -f /etc/haproxy/haproxy.cfg

Combined with -c, the option checks the configuration while adding these warnings, without starting an instance.

Hands-on practice

Move from the configuration you read to the configuration you write: this lab has you declare a frontend and a backend of several web servers in HAProxy, then observe how traffic is spread across them on a VM.

Key points

  1. HAProxy is a reverse proxy and load balancer that spreads traffic across several servers for high availability and performance.

  2. The frontend/backend blocks separate receiving traffic (frontend) from distributing it (backend).

  3. Health checks automatically remove failing servers from the pool: always enable them with check.

  4. Sticky sessions (cookies) keep client-server affinity for stateful applications.

  5. Rate limiting protects your backends against abuse: configure a stick-table with reasonable limits.

  6. Always validate the configuration with haproxy -c before reloading.

  7. The statistics page (port 8404) gives a real-time view of the state of your infrastructure.

Knowledge check

Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

More information

The configuration shown here covers only part of the directives available. The manual of the 3.2 branch remains the source to consult as soon as a directive falls outside the scope of this guide, notably for the resolvers sections, peers and the stick-table converters.

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn