HAProxy Load Balancing: From Installation to Production
HAProxy (High Availability Proxy) is the industry standard for TCP and HTTP load balancing. It powers some of the largest sites on the internet, including GitHub, Reddit, Stack Overflow, and Tumblr, handling millions of concurrent connections with predictable, sub-millisecond latency. If you are running more than one backend server and need reliable traffic distribution, HAProxy should be at the top of your shortlist.
This guide walks through everything from installation to a hardened, production-ready configuration. By the end, you will have a complete understanding of HAProxy's architecture, every major configuration section, advanced routing with ACLs, SSL/TLS termination, health checking strategies, rate limiting, logging pipelines, performance tuning, and monitoring via the built-in stats dashboard and Prometheus.
Why HAProxy
HAProxy sits in front of your application servers and distributes incoming traffic across them. The benefits go well beyond simple round-robin distribution:
- Horizontal scaling -- add backend servers without changing client-facing endpoints. Your DNS always points to HAProxy, and you add or remove backends behind it at will.
- Zero-downtime deployments -- drain connections from a server using the admin socket, deploy your new code, run smoke tests, then re-enable the server. Clients never see an error.
- Health checking -- automatically remove unhealthy backends from the rotation. HAProxy supports HTTP, TCP, and custom script-based health checks with configurable thresholds.
- SSL termination -- offload TLS handshake overhead from application servers. HAProxy handles certificate management, OCSP stapling, and cipher negotiation in one place.
- Observability -- built-in stats dashboard showing real-time connection counts, error rates, response times, and queue depths. A native Prometheus exporter means you can feed metrics directly into Grafana.
- Rate limiting -- stick tables let you track request rates, connection counts, and byte rates per client IP, then throttle or deny abusive traffic without any external tooling.
- Content switching -- ACLs let you route traffic based on hostname, URL path, HTTP method, headers, cookies, source IP, or any combination.
Understanding the Traffic Flow
Before diving into configuration, it helps to visualize how traffic moves through HAProxy. Here is the flow from client to backend:
Client Request
|
v
+---------------------+
| Frontend Section | Binds to IP:port, accepts connections,
| (bind, ACLs) | evaluates ACLs, selects backend
+---------------------+
|
v
+---------------------+
| Backend Section | Applies load balancing algorithm,
| (servers, checks) | runs health checks, routes to server
+---------------------+
|
v
+---------------------+
| Application Server | Processes request, returns response
+---------------------+
|
v
Client receives response
The global section configures process-level behavior (logging, security, performance limits). The defaults section sets fallback values for all frontends and backends. Frontends define where HAProxy listens and how it classifies incoming traffic. Backends define pools of servers and how traffic is distributed among them.
Installation
Ubuntu / Debian
The default repository version is usually outdated by several major releases. Use the official PPA maintained by Vincent Bernat for the latest stable branch:
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:vbernat/haproxy-2.8
sudo apt-get update
sudo apt-get install -y haproxy=2.8.*
Verify the installation:
haproxy -v
# outputs: HAProxy version 2.8.x ...
RHEL / Rocky / Alma
sudo dnf install -y epel-release
sudo dnf install -y haproxy
For a newer version, build from source or use the IUS repository.
Docker
For containerized environments, the Alpine-based image is lightweight and production-ready:
docker run -d \
--name haproxy \
--restart unless-stopped \
-v /etc/haproxy:/usr/local/etc/haproxy:ro \
-p 80:80 -p 443:443 -p 8404:8404 \
--sysctl net.ipv4.ip_nonlocal_bind=1 \
--sysctl net.core.somaxconn=65535 \
haproxy:2.8-alpine
The net.core.somaxconn sysctl increases the socket listen backlog, which is essential for high-traffic deployments where thousands of connections arrive simultaneously.
Building from Source
For environments requiring custom compile-time options (such as QUIC support or a specific OpenSSL version):
wget https://www.haproxy.org/download/2.8/src/haproxy-2.8.5.tar.gz
tar xzf haproxy-2.8.5.tar.gz
cd haproxy-2.8.5
make -j$(nproc) \
TARGET=linux-glibc \
USE_OPENSSL=1 \
USE_PCRE2=1 \
USE_LUA=1 \
USE_SYSTEMD=1 \
USE_PROMEX=1 \
USE_QUIC=1
sudo make install
The USE_PROMEX=1 flag compiles in the native Prometheus exporter. USE_QUIC=1 enables HTTP/3 support, which is useful if you plan to serve QUIC traffic.
Configuration File Structure
HAProxy's configuration lives in /etc/haproxy/haproxy.cfg and is divided into four main sections. Understanding these sections is the key to mastering HAProxy.
Global Section
Controls process-level settings that apply to the HAProxy daemon itself:
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
# Performance tuning
maxconn 100000
nbthread 4
cpu-map auto:1/1-4 0-3
tune.bufsize 32768
tune.maxrewrite 1024
tune.ssl.default-dh-param 2048
tune.ssl.cachesize 100000
tune.ssl.lifetime 600
tune.h2.max-concurrent-streams 128
# SSL tuning
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-server-options ssl-min-ver TLSv1.2
Key parameters explained:
| Parameter | Purpose | Recommended Value |
|---|---|---|
maxconn | Maximum concurrent connections across all frontends | 50000-200000 depending on RAM |
nbthread | Number of threads (replaces nbproc in 2.5+) | Match CPU core count |
cpu-map | Pin threads to specific CPU cores | auto:1/1-N 0-(N-1) |
tune.bufsize | Size of per-connection I/O buffers | 16384 (default) to 32768 |
tune.ssl.cachesize | Number of SSL sessions cached | 100000 for high-traffic sites |
tune.h2.max-concurrent-streams | HTTP/2 stream multiplexing limit | 100-256 |
chroot | Jails the process for security | Always enable in production |
The stats socket with expose-fd listeners allows seamless reloads. When HAProxy reloads, the new process inherits listening file descriptors from the old one, so no connections are dropped.
Defaults Section
Sets fallback values for all frontend and backend sections. Any value defined here can be overridden in individual frontends or backends:
defaults
log global
mode http
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
timeout connect 5s
timeout client 30s
timeout server 30s
timeout http-request 10s
timeout http-keep-alive 4s
timeout queue 30s
timeout tunnel 3600s
timeout tarpit 60s
# Retry configuration
retries 3
retry-on conn-failure empty-response response-timeout
# Error files
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
Timeout explanations:
| Timeout | When It Applies | Impact If Too Low |
|---|---|---|
connect | Time to establish a TCP connection to a backend | Backends behind slow networks get marked down |
client | Maximum inactivity on the client side | Slow clients get disconnected |
server | Maximum inactivity on the server side | Long-running queries get killed |
http-request | Time to receive the complete HTTP request headers | Protects against slowloris attacks |
http-keep-alive | Idle time between two requests on a keep-alive connection | Controls keep-alive resource usage |
queue | Time a request can wait in the queue for a server slot | Prevents indefinite queuing |
tunnel | Timeout for WebSocket and CONNECT tunnels | Set high for long-lived connections |
Frontend Section
Defines how incoming connections are received and classified:
frontend http_front
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/site.pem alpn h2,http/1.1
# Redirect HTTP to HTTPS
http-request redirect scheme https unless { ssl_fc }
# Add 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"
# Forward client info
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Real-IP %[src]
http-request set-header X-Request-ID %[uuid()]
# Route based on hostname
acl is_api hdr(host) -i api.example.com
acl is_web hdr(host) -i www.example.com
acl is_admin hdr(host) -i admin.example.com
# Route based on path
acl is_static path_beg /static/ /images/ /css/ /js/
acl is_websocket hdr(Upgrade) -i websocket
use_backend api_servers if is_api
use_backend admin_servers if is_admin
use_backend static_servers if is_static
use_backend websocket_servers if is_websocket
default_backend web_servers
Backend Section
Defines the pool of servers that handle requests:
backend web_servers
balance roundrobin
option httpchk GET /health
http-check expect status 200
cookie SERVERID insert indirect nocache
# Connection pooling to backends
http-reuse safe
server web1 10.0.1.10:8080 check inter 5s fall 3 rise 2 cookie w1 maxconn 500
server web2 10.0.1.11:8080 check inter 5s fall 3 rise 2 cookie w2 maxconn 500
server web3 10.0.1.12:8080 check inter 5s fall 3 rise 2 cookie w3 maxconn 500
server web-backup 10.0.1.20:8080 check inter 5s fall 3 rise 2 backup
The backup keyword on the last server means it only receives traffic when all primary servers are down. The maxconn 500 per server prevents any single backend from being overwhelmed.
HTTP Mode vs TCP Mode
HAProxy operates in two fundamental modes, and choosing the right one is critical:
| Feature | HTTP Mode (mode http) | TCP Mode (mode tcp) |
|---|---|---|
| OSI Layer | Layer 7 (Application) | Layer 4 (Transport) |
| Routing | URL path, headers, cookies, query strings | IP address and port only |
| SSL inspection | Can terminate SSL, inspect headers | Passes SSL through (passthrough) |
| Content modification | Can add/remove/modify headers | No content awareness |
| Health checks | HTTP GET/HEAD with status/body checks | TCP connect, send/expect |
| Stick tables | Can track by URL, cookie, header | Can track by source IP |
| Use case | Web apps, REST APIs, GraphQL | Databases, SMTP, raw TCP services |
| Performance | Slightly more CPU overhead due to parsing | Minimal overhead, highest throughput |
| Connection reuse | Full HTTP keep-alive and multiplexing | Pass-through only |
For TCP mode, here is a production example for load balancing PostgreSQL with proper health checking:
frontend pg_front
mode tcp
bind *:5432
default_backend pg_primary
backend pg_primary
mode tcp
balance roundrobin
option tcp-check
# PostgreSQL health check: connect and verify it accepts queries
tcp-check connect
tcp-check send-binary 00000028 # packet length
tcp-check send-binary 00030000 # protocol version 3.0
tcp-check send-binary 7573657200 # "user\0"
tcp-check send-binary 686170726f787900 # "haproxy\0"
tcp-check send-binary 00 # end of parameters
tcp-check expect binary 52 # expect Authentication response
server pg1 10.0.2.10:5432 check inter 3s fall 3 rise 2 maxconn 200
server pg2 10.0.2.11:5432 check inter 3s fall 3 rise 2 maxconn 200
backend pg_replicas
mode tcp
balance leastconn
option tcp-check
tcp-check connect
server pg-replica1 10.0.2.20:5432 check inter 3s fall 3 rise 2
server pg-replica2 10.0.2.21:5432 check inter 3s fall 3 rise 2
Another common TCP use case is Redis sentinel-aware load balancing:
backend redis_servers
mode tcp
balance first
option tcp-check
tcp-check connect
tcp-check send "PING\r\n"
tcp-check expect string +PONG
tcp-check send "INFO replication\r\n"
tcp-check expect string role:master
tcp-check send "QUIT\r\n"
tcp-check expect string +OK
server redis1 10.0.3.10:6379 check inter 2s fall 3 rise 2
server redis2 10.0.3.11:6379 check inter 2s fall 3 rise 2
server redis3 10.0.3.12:6379 check inter 2s fall 3 rise 2
This configuration checks not only that Redis responds but that it is the master node, ensuring writes always go to the correct instance.
Load Balancing Algorithms
HAProxy supports several algorithms. Choose based on your workload characteristics:
| Algorithm | Directive | Behavior | Best For |
|---|---|---|---|
| Round Robin | roundrobin | Rotates through servers sequentially, respects weights. Dynamic weight adjustment. Max 4095 servers. | General purpose, stateless apps |
| Static Round Robin | static-rr | Like roundrobin but does not support dynamic weight changes. No server limit. | Large server pools |
| Least Connections | leastconn | Routes to the server with the fewest active connections | Long-lived connections, WebSockets, database proxying |
| Source Hash | source | Hashes client source IP for consistent server mapping | Session affinity without cookies |
| URI Hash | uri | Hashes the request URI for consistent routing | Cache servers, content distribution |
| URL Parameter | url_param userid | Hashes a specific URL parameter | User-specific routing |
| Header Hash | hdr(name) | Hashes a specific HTTP header value | Tenant-based routing, API key routing |
| Random | random(2) | Picks two random servers, selects the one with fewer connections | Large clusters where leastconn tracking is expensive |
| First Available | first | Fills servers in order of their declaration | Minimizing active server count for cost |
Example with weighted round robin and slow-start:
backend web_servers
balance roundrobin
server web1 10.0.1.10:8080 weight 3 check slowstart 30s
server web2 10.0.1.11:8080 weight 2 check slowstart 30s
server web3 10.0.1.12:8080 weight 1 check slowstart 30s
The slowstart 30s parameter gradually increases traffic to a server that just came back online. Instead of immediately receiving its full share, the server ramps up over 30 seconds. This is critical for applications that need to warm caches or JIT-compile code.
Server web1 receives 50% of traffic (weight 3 out of 6 total), web2 gets 33%, and web3 gets 17%.
Health Checks
Health checks are critical for reliability. Without them, HAProxy sends traffic to dead servers and your users see errors.
HTTP Health Checks
backend app_servers
option httpchk GET /healthz HTTP/1.1
http-check send hdr Host www.example.com
http-check expect status 200
http-check expect body "status":"ok"
# inter: check interval
# fall: consecutive failures before marking down
# rise: consecutive successes before marking up
server app1 10.0.1.10:8080 check inter 3s fall 3 rise 2
server app2 10.0.1.11:8080 check inter 3s fall 3 rise 2
Advanced HTTP Health Checks
HAProxy 2.2+ supports multi-step HTTP health checks with the http-check directive:
backend app_servers
option httpchk
http-check connect
http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host www.example.com
http-check expect status 200
http-check send meth GET uri /readyz ver HTTP/1.1 hdr Host www.example.com
http-check expect status 200
server app1 10.0.1.10:8080 check inter 5s fall 3 rise 2
This performs two sequential HTTP requests, both of which must succeed. Use this pattern when you want to verify that a server is both healthy (can serve traffic) and ready (has completed startup tasks like loading data into memory).
TCP Health Checks
For non-HTTP services:
backend redis_servers
mode tcp
option tcp-check
tcp-check connect
tcp-check send "PING\r\n"
tcp-check expect string +PONG
server redis1 10.0.3.10:6379 check inter 2s
Agent Health Checks
HAProxy can query an external agent on a separate port. The agent returns the server's weight or state:
backend app_servers
server app1 10.0.1.10:8080 check inter 5s agent-check agent-port 8081 agent-inter 3s
The agent on port 8081 can return values like up, down, 50% (set weight to 50%), or drain (stop sending new connections). This is powerful for application-aware load balancing where the application itself reports its capacity.
Health Check Timing Guidelines
| Environment | inter | fall | rise | Effect |
|---|---|---|---|---|
| Latency-sensitive | 2s | 2 | 1 | Fast detection (4s to mark down), quick recovery |
| Standard web app | 5s | 3 | 2 | Balanced detection (15s), avoids false positives |
| Database backend | 10s | 3 | 3 | Conservative (30s), avoids unnecessary failovers |
| Background workers | 30s | 5 | 3 | Very conservative, workers can tolerate brief outages |
ACLs and Routing Rules
ACLs (Access Control Lists) give you fine-grained traffic routing. They match conditions on requests and let you route, block, or modify traffic based on virtually any attribute.
frontend http_front
bind *:443 ssl crt /etc/haproxy/certs/site.pem
# Path-based routing
acl is_api path_beg /api/
acl is_static path_beg /static/ /images/ /css/ /js/
acl is_graphql path /graphql
# Header-based routing
acl is_mobile hdr_sub(User-Agent) -i mobile android iphone
acl is_grpc hdr(Content-Type) -i application/grpc
# Method-based access control
acl is_delete method DELETE
acl is_options method OPTIONS
# Source IP whitelisting
acl is_internal src 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
acl is_office src 203.0.113.0/24
# Query string matching
acl is_debug url_param(debug) -m str true
# Time-based rules
acl is_business_hours date_hour 9-17
acl is_weekday date_dow 1-5
# Rate limiting with stick tables (covered below)
acl is_abuser sc_http_req_rate(0) gt 100
# CORS preflight handling
http-request return status 200 content-type text/plain if is_options
# Block DELETE from outside the office
http-request deny deny_status 403 if is_delete !is_office
# Routing decisions
use_backend grpc_servers if is_grpc
use_backend api_servers if is_api
use_backend cdn_servers if is_static
use_backend graphql_servers if is_graphql
default_backend web_servers
ACLs are evaluated in order. The first use_backend whose ACL matches wins. You can combine ACLs with if, unless, || (or), and ! (not) operators.
SSL/TLS Termination
HAProxy can terminate SSL so your backend servers handle only plain HTTP, simplifying certificate management and improving performance.
Certificate Preparation
Combine your certificate and key into a single PEM file:
cat /etc/letsencrypt/live/example.com/fullchain.pem \
/etc/letsencrypt/live/example.com/privkey.pem \
> /etc/haproxy/certs/example.com.pem
chmod 600 /etc/haproxy/certs/example.com.pem
Frontend Configuration
frontend https_front
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
# Force HTTPS
http-request redirect scheme https code 301 unless { ssl_fc }
# Forward protocol info
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Real-IP %[src]
# HSTS header
http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
The crt /etc/haproxy/certs/ directive tells HAProxy to load all PEM files in that directory and use SNI (Server Name Indication) to select the correct certificate automatically. This means you can serve multiple domains from a single frontend.
SSL Tuning for Performance
global
# Use OpenSSL async mode for hardware offloading
ssl-engine rdrand
ssl-mode-async
# SSL session cache (reduces handshake overhead)
tune.ssl.cachesize 100000
tune.ssl.lifetime 600
# Modern cipher configuration
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
Mutual TLS (mTLS)
For service-to-service communication where both sides verify certificates:
frontend mtls_front
bind *:8443 ssl crt /etc/haproxy/certs/server.pem ca-file /etc/haproxy/certs/ca.pem verify required
# Extract client certificate info
http-request set-header X-Client-CN %[ssl_c_s_dn(cn)]
http-request set-header X-Client-Serial %[ssl_c_serial,hex]
Stats Dashboard
HAProxy includes a built-in stats page that shows server status, connection counts, error rates, queue depths, and response times in real time.
frontend stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
stats admin if LOCALHOST
stats auth admin:your_secure_password
stats show-legends
stats show-node
Access it at http://your-server:8404/stats. The admin interface (enabled by stats admin if LOCALHOST) allows you to drain servers, set weights, and toggle server states from the web UI.
Prometheus Integration
For modern monitoring stacks, add the Prometheus exporter:
frontend stats
bind *:8404
http-request use-service prometheus-exporter if { path /metrics }
stats enable
stats uri /stats
stats refresh 10s
Key Prometheus metrics to alert on:
| Metric | Alert Threshold | Meaning |
|---|---|---|
haproxy_backend_up | 0 | All servers in a backend are down |
haproxy_server_status | != 1 | Individual server health |
haproxy_backend_http_responses_total{code="5xx"} | Rate spike | Backend errors increasing |
haproxy_backend_queue_current | > 0 sustained | Requests are queuing, backends overloaded |
haproxy_frontend_current_sessions | Near maxconn | Running out of connection capacity |
haproxy_backend_response_time_average_seconds | > SLA target | Backend latency degradation |
Rate Limiting with Stick Tables
Stick tables are one of HAProxy's most powerful features. They are in-memory key-value stores that track per-client metrics and enable sophisticated rate limiting.
frontend http_front
bind *:443 ssl crt /etc/haproxy/certs/site.pem
# Track request rates per source IP
stick-table type ip size 200k expire 30s store http_req_rate(10s),conn_cur,bytes_out_rate(60s)
http-request track-sc0 src
# Deny IPs exceeding 100 requests in 10 seconds
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
# Deny IPs with more than 50 concurrent connections
http-request deny deny_status 429 if { sc_conn_cur(0) gt 50 }
# Tarpit (slow response) for suspected bots instead of blocking
http-request tarpit if { sc_http_req_rate(0) gt 200 }
Multi-Layer Rate Limiting
You can stack multiple stick tables for different rate limiting strategies:
frontend http_front
bind *:443 ssl crt /etc/haproxy/certs/site.pem
# Table 1: Per-IP request rate (general)
stick-table type ip size 200k expire 60s store http_req_rate(10s)
http-request track-sc0 src table per_ip_rate
# Table 2: Per-IP + path rate (API-specific)
stick-table type binary len 40 size 100k expire 30s store http_req_rate(10s)
http-request track-sc1 src,path table per_path_rate if { path_beg /api/ }
# Apply limits
http-request deny deny_status 429 if { sc_http_req_rate(0,per_ip_rate) gt 200 }
http-request deny deny_status 429 if { sc_http_req_rate(1,per_path_rate) gt 50 }
backend per_ip_rate
stick-table type ip size 200k expire 60s store http_req_rate(10s)
backend per_path_rate
stick-table type binary len 40 size 100k expire 30s store http_req_rate(10s)
Inspecting Stick Tables at Runtime
Use the admin socket to inspect stick table contents:
echo "show table per_ip_rate" | socat stdio /run/haproxy/admin.sock
echo "clear table per_ip_rate key 10.0.0.50" | socat stdio /run/haproxy/admin.sock
Logging
HAProxy logs to syslog. Configure rsyslog to capture HAProxy logs in a dedicated file:
# /etc/rsyslog.d/49-haproxy.conf
$AddUnixListenSocket /var/lib/haproxy/dev/log
local0.* /var/log/haproxy/haproxy.log
local1.notice /var/log/haproxy/haproxy-admin.log
Restart rsyslog after creating this file:
sudo systemctl restart rsyslog
Structured JSON Logging
For log aggregation systems like ELK, Loki, or Datadog:
defaults
log-format '{"time":"%t","client":"%ci:%cp","frontend":"%f","backend":"%b","server":"%s","method":"%HM","uri":"%HP","status":%ST,"bytes_read":%B,"time_total":%Tt,"time_queue":%Tw,"time_connect":%Tc,"time_response":%Tr,"retries":%rc,"conn_active":%ac,"conn_frontend":%fc,"conn_backend":%bc,"conn_server":%sc}'
This log format captures timing breakdowns for every request, letting you pinpoint whether slowness is in queueing (Tw), connecting to the backend (Tc), or the backend processing time (Tr).
Log Rotation
# /etc/logrotate.d/haproxy
/var/log/haproxy/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
postrotate
/usr/lib/rsyslog/rsyslog-rotate
endscript
}
Performance Tuning
System-Level Tuning
HAProxy can only perform as well as the kernel allows. Apply these sysctl settings on your load balancer:
# /etc/sysctl.d/99-haproxy.conf
# Increase connection tracking and socket buffers
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
# Increase available local port range
net.ipv4.ip_local_port_range = 1024 65535
# Enable TCP reuse and recycling
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# Increase file descriptors
fs.file-max = 2097152
# TCP keepalive tuning
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 6
# Buffer sizes
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
Apply with sysctl -p /etc/sysctl.d/99-haproxy.conf.
Also raise the file descriptor limit for the haproxy user:
# /etc/security/limits.d/haproxy.conf
haproxy soft nofile 1000000
haproxy hard nofile 1000000
HAProxy-Level Tuning
global
maxconn 100000
nbthread 4
# Spread connections evenly across threads
tune.listener.multi-queue on
# Connection reuse between frontend and backend
tune.idle-pool.shared on
Connection Reuse
HTTP connection reuse dramatically reduces backend connection overhead:
backend web_servers
# 'safe' reuses connections only for requests that can be safely retried
http-reuse safe
# For trusted backends where all requests are idempotent
# http-reuse always
Production Configuration Example
Here is a complete, hardened configuration for a production web application:
global
log /dev/log local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
user haproxy
group haproxy
daemon
maxconn 100000
nbthread 4
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
tune.ssl.default-dh-param 2048
tune.ssl.cachesize 100000
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
option http-server-close
timeout connect 5s
timeout client 30s
timeout server 30s
timeout http-request 10s
timeout http-keep-alive 4s
timeout queue 30s
retries 3
retry-on conn-failure empty-response response-timeout
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend https_in
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
http-request redirect scheme https code 301 unless { ssl_fc }
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Real-IP %[src]
http-request set-header X-Request-ID %[uuid()]
# Security headers
http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
http-response set-header X-Frame-Options "SAMEORIGIN"
http-response set-header X-Content-Type-Options "nosniff"
# Rate limiting
stick-table type ip size 200k expire 30s store http_req_rate(10s),conn_cur
http-request track-sc0 src
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 200 }
http-request deny deny_status 429 if { sc_conn_cur(0) gt 100 }
# Routing
acl is_api path_beg /api/
acl is_static path_beg /static/ /assets/
acl is_websocket hdr(Upgrade) -i websocket
use_backend api_servers if is_api
use_backend static_servers if is_static
use_backend ws_servers if is_websocket
default_backend web_servers
backend web_servers
balance roundrobin
option httpchk GET /health HTTP/1.1
http-check send hdr Host www.example.com
http-check expect status 200
http-reuse safe
cookie SERVERID insert indirect nocache
server web1 10.0.1.10:3000 check inter 5s fall 3 rise 2 cookie w1 maxconn 1000 slowstart 30s
server web2 10.0.1.11:3000 check inter 5s fall 3 rise 2 cookie w2 maxconn 1000 slowstart 30s
backend api_servers
balance leastconn
option httpchk GET /api/health HTTP/1.1
http-check send hdr Host api.example.com
http-check expect status 200
http-reuse safe
server api1 10.0.2.10:8080 check inter 3s fall 3 rise 2 maxconn 500
server api2 10.0.2.11:8080 check inter 3s fall 3 rise 2 maxconn 500
backend static_servers
balance roundrobin
server static1 10.0.3.10:80 check inter 10s fall 3 rise 2
backend ws_servers
balance source
timeout tunnel 3600s
server ws1 10.0.4.10:8080 check inter 5s fall 3 rise 2
server ws2 10.0.4.11:8080 check inter 5s fall 3 rise 2
frontend stats
bind *:8404
http-request use-service prometheus-exporter if { path /metrics }
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:changeme_in_production
stats show-legends
Validation, Reload, and Troubleshooting
Validation and Reload
Always validate before reloading:
# Check config syntax
haproxy -c -f /etc/haproxy/haproxy.cfg
# Graceful reload (zero downtime with expose-fd listeners)
sudo systemctl reload haproxy
# Check status
sudo systemctl status haproxy
Runtime Management via Admin Socket
The admin socket provides powerful runtime control:
# Show server states
echo "show servers state" | socat stdio /run/haproxy/admin.sock
# Drain a server (stop new connections, finish existing)
echo "set server web_servers/web1 state drain" | socat stdio /run/haproxy/admin.sock
# Re-enable a server
echo "set server web_servers/web1 state ready" | socat stdio /run/haproxy/admin.sock
# Change server weight at runtime
echo "set server web_servers/web1 weight 50%" | socat stdio /run/haproxy/admin.sock
# Show current sessions
echo "show sess" | socat stdio /run/haproxy/admin.sock
# Show stick table contents
echo "show table http_front" | socat stdio /run/haproxy/admin.sock
# Show info (memory, connections, uptime)
echo "show info" | socat stdio /run/haproxy/admin.sock
Common Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| 503 errors | All backends down or maxconn reached | Check health checks, raise maxconn |
| Connection timeouts | timeout connect too low or backend unreachable | Test backend connectivity, raise timeout |
| Intermittent 502 | Backend closing connections unexpectedly | Enable option http-server-close, check backend keep-alive settings |
| High queue time | Backend too slow, maxconn per-server too low | Add more backends or raise maxconn |
| SSL handshake failures | Certificate mismatch or expired cert | Verify PEM file contents, check expiry with openssl x509 -enddate |
| Stick table filling up | Too many unique clients, table too small | Increase size parameter or reduce expire |
Key Takeaways
- Start with
mode httpandroundrobinunless you have a specific reason not to. These defaults handle the vast majority of workloads. - Always enable health checks with appropriate
inter,fall, andrisevalues. Use HTTP-level checks that verify your application is actually working, not just that the port is open. - Use ACLs for content-based routing rather than running multiple HAProxy instances. A single frontend with well-organized ACLs is easier to manage and debug.
- Terminate SSL at HAProxy and pass plain HTTP to backends. This centralizes certificate management and offloads cryptographic overhead.
- Enable the stats page and Prometheus exporter from day one. They are your first line of debugging and capacity planning.
- Use stick tables for rate limiting instead of external tools. They are fast, in-memory, and require no additional infrastructure.
- Tune your system kernel (
somaxconn,file-max, TCP settings) alongside HAProxy configuration. The proxy cannot exceed what the kernel allows. - Use
slowstarton servers to prevent a freshly restarted backend from being overwhelmed. - Use the admin socket for zero-downtime deployments: drain, deploy, verify, re-enable.
- Validate every config change with
haproxy -cbefore reloading. A bad reload takes down your load balancer.
HAProxy is reliable, fast, and thoroughly documented. Once you understand the four-section config structure -- global, defaults, frontend, backend -- everything else is combining ACLs, health checks, and algorithms to match your architecture. The investment in learning HAProxy pays dividends every time you need to scale, deploy, or debug a production system.
SRE & Observability Engineer
If it's not measured, it doesn't exist. SLO-driven, metrics-obsessed, and the person who gets paged at 3 AM so you don't have to. Observability isn't optional.
Related Articles
Keepalived VRRP: Automatic Failover for Load Balancers
Implement automatic failover for HAProxy and Nginx using Keepalived with VRRP — virtual IPs, health checks, and split-brain prevention.
Azure Core Services: The DevOps Engineer's Essential Guide
Understand Azure's essential services — VMs, Storage, VNets, Azure AD (Entra ID), AKS, App Service, and Azure DevOps for infrastructure automation.
The Complete AWS Cost Optimization Playbook: Compute, Storage, Networking, and Reserved Capacity
A data-driven playbook for cutting AWS costs across compute, storage, networking, and reserved capacity with real numbers and actions.