DevOpsil
HAProxy
93%
Fresh

HAProxy Load Balancing: From Installation to Production

Riku TanakaRiku Tanaka24 min read

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:

ParameterPurposeRecommended Value
maxconnMaximum concurrent connections across all frontends50000-200000 depending on RAM
nbthreadNumber of threads (replaces nbproc in 2.5+)Match CPU core count
cpu-mapPin threads to specific CPU coresauto:1/1-N 0-(N-1)
tune.bufsizeSize of per-connection I/O buffers16384 (default) to 32768
tune.ssl.cachesizeNumber of SSL sessions cached100000 for high-traffic sites
tune.h2.max-concurrent-streamsHTTP/2 stream multiplexing limit100-256
chrootJails the process for securityAlways 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:

TimeoutWhen It AppliesImpact If Too Low
connectTime to establish a TCP connection to a backendBackends behind slow networks get marked down
clientMaximum inactivity on the client sideSlow clients get disconnected
serverMaximum inactivity on the server sideLong-running queries get killed
http-requestTime to receive the complete HTTP request headersProtects against slowloris attacks
http-keep-aliveIdle time between two requests on a keep-alive connectionControls keep-alive resource usage
queueTime a request can wait in the queue for a server slotPrevents indefinite queuing
tunnelTimeout for WebSocket and CONNECT tunnelsSet 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:

FeatureHTTP Mode (mode http)TCP Mode (mode tcp)
OSI LayerLayer 7 (Application)Layer 4 (Transport)
RoutingURL path, headers, cookies, query stringsIP address and port only
SSL inspectionCan terminate SSL, inspect headersPasses SSL through (passthrough)
Content modificationCan add/remove/modify headersNo content awareness
Health checksHTTP GET/HEAD with status/body checksTCP connect, send/expect
Stick tablesCan track by URL, cookie, headerCan track by source IP
Use caseWeb apps, REST APIs, GraphQLDatabases, SMTP, raw TCP services
PerformanceSlightly more CPU overhead due to parsingMinimal overhead, highest throughput
Connection reuseFull HTTP keep-alive and multiplexingPass-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:

AlgorithmDirectiveBehaviorBest For
Round RobinroundrobinRotates through servers sequentially, respects weights. Dynamic weight adjustment. Max 4095 servers.General purpose, stateless apps
Static Round Robinstatic-rrLike roundrobin but does not support dynamic weight changes. No server limit.Large server pools
Least ConnectionsleastconnRoutes to the server with the fewest active connectionsLong-lived connections, WebSockets, database proxying
Source HashsourceHashes client source IP for consistent server mappingSession affinity without cookies
URI HashuriHashes the request URI for consistent routingCache servers, content distribution
URL Parameterurl_param useridHashes a specific URL parameterUser-specific routing
Header Hashhdr(name)Hashes a specific HTTP header valueTenant-based routing, API key routing
Randomrandom(2)Picks two random servers, selects the one with fewer connectionsLarge clusters where leastconn tracking is expensive
First AvailablefirstFills servers in order of their declarationMinimizing 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

EnvironmentinterfallriseEffect
Latency-sensitive2s21Fast detection (4s to mark down), quick recovery
Standard web app5s32Balanced detection (15s), avoids false positives
Database backend10s33Conservative (30s), avoids unnecessary failovers
Background workers30s53Very 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:

MetricAlert ThresholdMeaning
haproxy_backend_up0All servers in a backend are down
haproxy_server_status!= 1Individual server health
haproxy_backend_http_responses_total{code="5xx"}Rate spikeBackend errors increasing
haproxy_backend_queue_current> 0 sustainedRequests are queuing, backends overloaded
haproxy_frontend_current_sessionsNear maxconnRunning out of connection capacity
haproxy_backend_response_time_average_seconds> SLA targetBackend 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

SymptomLikely CauseFix
503 errorsAll backends down or maxconn reachedCheck health checks, raise maxconn
Connection timeoutstimeout connect too low or backend unreachableTest backend connectivity, raise timeout
Intermittent 502Backend closing connections unexpectedlyEnable option http-server-close, check backend keep-alive settings
High queue timeBackend too slow, maxconn per-server too lowAdd more backends or raise maxconn
SSL handshake failuresCertificate mismatch or expired certVerify PEM file contents, check expiry with openssl x509 -enddate
Stick table filling upToo many unique clients, table too smallIncrease size parameter or reduce expire

Key Takeaways

  • Start with mode http and roundrobin unless you have a specific reason not to. These defaults handle the vast majority of workloads.
  • Always enable health checks with appropriate inter, fall, and rise values. 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 slowstart on 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 -c before 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.

Share:
Riku Tanaka
Riku Tanaka

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