You need to put HAProxy in front of three web servers, terminate SSL for all of them, route traffic based on domain or path, and stop sending requests to any backend that goes down. HAProxy handles this workload better than most alternatives, but it requires precise haproxy configuration: one typo in a bind line or ACL rule can take your site offline. Before you restart the service, always run haproxy -c -f /etc/haproxy/haproxy.cfg to catch syntax errors. A clean validation means the config parses; it does not guarantee your logic is correct, but it prevents most self-inflicted outages.
This guide walks through the three pillars of a production setup: SSL termination with modern ciphers and HSTS, health checks that mark backends down before your users notice, and ACL-based routing to send requests to the right backend pool. Each section includes working configuration snippets and the commands you use to troubleshoot when something breaks. If you are migrating from Nginx or setting up your first load balancer, the patterns here apply to HAProxy 2.4 and later on any distribution.
Installing HAProxy and Config Structure
On Debian and Ubuntu, apt update && apt install haproxy installs the current stable release from the distribution repository. For RHEL, Rocky or AlmaLinux, dnf install haproxy does the same. The default configuration file lives at /etc/haproxy/haproxy.cfg. HAProxy reads this file on startup and does not auto-reload when you edit it; you must validate and restart or reload the service.
The config is divided into four sections. global sets process-wide options like user, log destination and SSL defaults. defaults applies common settings to all frontends and backends unless overridden. frontend defines listening sockets and initial request handling. backend lists the upstream servers and health check rules. A minimal working config looks like this:
global
log /dev/log local0
maxconn 4096
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
timeout connect 5s
timeout client 30s
timeout server 30s
frontend http_front
bind *:80
default_backend web_servers
backend web_servers
server web1 127.0.0.1:8080 check
This frontend listens on port 80 and forwards all requests to a single backend server on localhost. Before you apply any change, run haproxy -c -f /etc/haproxy/haproxy.cfg. If the output says "Configuration file is valid," you can reload with systemctl reload haproxy or restart with systemctl restart haproxy. Enable the service at boot with systemctl enable haproxy. For deeper configuration management patterns, see Linux Environment Variables & Configuration Management.
SSL Termination in the Frontend
HAProxy handles SSL termination in the frontend, decrypting client connections and forwarding plain HTTP to backends. This offloads CPU-intensive crypto from application servers and centralizes certificate management. The three steps are combining your certificate files, configuring the bind directive, and enforcing modern cipher suites.
Certificate and Key Setup
HAProxy expects a single PEM file that contains the certificate, private key and any intermediate CA certificates in order: leaf certificate first, then intermediates, optionally the root CA last. Create this file with cat cert.pem key.pem intermediate.pem > /etc/haproxy/certs/example.com.pem. Set permissions to chmod 600 /etc/haproxy/certs/example.com.pem and chown haproxy:haproxy so only the HAProxy process can read it. If the chain is incomplete or out of order, clients will see certificate errors even though HAProxy starts without complaint.
HAProxy does not watch certificate files for changes. When you renew a certificate, replace the combined PEM and restart the service; a reload is not sufficient because the SSL context is built at startup. If you manage certificates with Let's Encrypt, your renewal hook should concatenate the new files and restart HAProxy.
Cipher Suites and TLS Versions
In the global section, set ssl-default-bind-ciphers and ssl-default-bind-options to enforce strong crypto. For TLS 1.2 and 1.3 with forward secrecy:
global
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-options ssl-min-ver TLSv1.2 no-tls-tickets
This disables TLS 1.0 and 1.1, which are deprecated, and turns off session tickets to improve forward secrecy. Run haproxy -vv to see the OpenSSL version HAProxy was built against and the full list of supported ciphers. If your distribution ships HAProxy 2.4 or later with OpenSSL 1.1.1 or 3.0, TLS 1.3 is available and requires no extra configuration beyond setting ssl-min-ver TLSv1.2. The HAProxy documentation provides a reference for all SSL bind options.
Forcing HTTPS and HSTS
In the frontend, bind port 443 with the SSL certificate and redirect all HTTP traffic to HTTPS:
frontend http_front
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
redirect scheme https code 301 if !{ ssl_fc }
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
default_backend web_servers
The redirect scheme https if !{ ssl_fc } line issues a 301 redirect for any request that did not arrive over SSL. The http-response set-header adds an HSTS header telling browsers to use HTTPS for the next year. Only add preload if you plan to submit your domain to the HSTS preload list; once preloaded, rolling back to HTTP is difficult.
Backend Health Checks
Health checks prevent HAProxy from routing requests to dead or overloaded backends. Without them, users see errors until you manually remove the failed server. HAProxy supports HTTP checks that request a specific path, TCP checks that just open a socket, and custom checks that run external scripts. HTTP checks are the most reliable for web applications.
HTTP Health Check Setup
In the backend section, add option httpchk and an http-check expect rule. Then append check to each server line:
backend web_servers
option httpchk GET /health
http-check expect status 200
server web1 192.168.1.10:80 check inter 5s rise 2 fall 3
server web2 192.168.1.11:80 check inter 5s rise 2 fall 3
server web3 192.168.1.12:80 check inter 5s rise 2 fall 3
HAProxy sends GET /health to each backend every 5 seconds. If the response status is 200, the check passes. A server must pass two consecutive checks (rise 2) to be marked up after being down, and fail three consecutive checks (fall 3) to be marked down. These thresholds prevent flapping when a backend is intermittently slow. If your health endpoint returns 200 but with a body that signals degraded state, add http-check expect string OK to verify the content.
Customizing Check Intervals and Thresholds
The inter parameter controls how often HAProxy checks a healthy server. For a backend under heavy load, 5 seconds is reasonable; for a lightly used service, 10 or 15 seconds reduces overhead. Use fastinter 2s to check a transitioning server (one that just failed or recovered) more frequently, and downinter 10s to back off when a server is confirmed down. Adjust rise and fall based on your tolerance for false positives: rise 3 fall 5 makes HAProxy slower to react but more forgiving of transient errors.
Add maxconn 100 to a server line to limit concurrent connections and prevent one backend from being overwhelmed while others are idle. This is especially useful when backends have different hardware specs. For a server that should only receive traffic during manual testing, append backup so HAProxy uses it only when all primary servers are down.
Troubleshooting Failed Checks
To see live backend status, enable the stats socket in the global section with stats socket /run/haproxy/admin.sock mode 660 level admin, then query it with echo 'show stat' | socat stdio /run/haproxy/admin.sock. Look for the status column: UP means healthy, DOWN means failed checks, MAINT means administratively disabled. The check_status column shows the last check result, such as L7OK for HTTP 200 or L7STS for a status code mismatch.
HAProxy logs check failures to syslog. A typical message looks like Health check for server web_servers/web1 failed, reason: Layer7 wrong status, code: 500. If you see Layer4 connection problem, the backend is not listening or a firewall is blocking HAProxy. If you see Layer7 timeout, the backend is too slow; increase timeout check 5s in the backend section. For TCP-only checks, replace option httpchk with option tcp-check; HAProxy will consider the server up if it can open a TCP connection, which is faster but less reliable for detecting application-level failures.
ACL-Based Routing
ACLs let you route requests to different backend pools based on hostname, path, headers or client IP. You define each ACL in the frontend with a name and a matching condition, then reference it in use_backend directives. HAProxy evaluates ACLs in the order you write them and stops at the first match, so place more specific rules before general ones.
Routing by Host Header
To send requests for api.example.com to a dedicated API backend and www.example.com to the main web servers, define two ACLs and two routing rules:
frontend http_front
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
acl is_api hdr(host) -i api.example.com
acl is_www hdr(host) -i www.example.com
use_backend api_pool if is_api
use_backend web_pool if is_www
default_backend web_pool
The hdr(host) fetches the HTTP Host header. The -i flag makes the match case-insensitive, so API.EXAMPLE.COM and api.example.com both trigger the ACL. If no ACL matches, HAProxy falls through to default_backend web_pool. Without a default backend, unmatched requests get a 503 error.
Path-Based Routing
Use path_beg to match the start of the URI path and path_end for the suffix. To route /admin requests to a separate backend and /api to the API pool:
acl is_admin path_beg /admin
acl is_api path_beg /api
use_backend admin_pool if is_admin
use_backend api_pool if is_api
default_backend web_pool
The path match includes the leading slash, so path_beg /admin matches /admin/users but not /administrator. For exact matches, use path /admin. For regex patterns, use path_reg ^/api/v[0-9]+, but be aware that regex ACLs are slower than prefix or exact matches. If you need to route based on query strings, use urlp(key) to extract a parameter value, such as acl is_mobile urlp(m) -m found to match any request with ?m=....
Combining ACLs
You can chain multiple ACLs with implicit AND logic by listing them on the same use_backend line. To route authenticated API requests to a secure backend:
acl is_api hdr(host) -i api.example.com
acl is_authenticated http_auth(userlist)
use_backend secure_api if is_api is_authenticated
use_backend api_pool if is_api
default_backend web_pool
Both is_api and is_authenticated must be true for the first rule to match. If the API request lacks valid credentials, it falls through to the second rule and goes to api_pool. For OR logic, define a third ACL: acl is_api_or_www is_api or is_www. Order matters: HAProxy stops at the first matching use_backend, so place the most specific rules first. If you put use_backend api_pool if is_api before the authenticated check, no request ever reaches secure_api.
Warning: HAProxy evaluates ACLs top to bottom and stops at the first match. If you place a broad rule like
use_backend web_pool if is_wwwbefore a more specificuse_backend admin_pool if is_www is_admin, the admin ACL never triggers.
Load Balancing Algorithms
The balance directive in each backend controls how HAProxy distributes requests across servers. The default is roundrobin, which works well for stateless HTTP where each request is independent. For long-lived connections or session-dependent applications, you need a different algorithm or sticky sessions.
Choosing Between roundrobin, leastconn, and source
Use balance roundrobin when backends are identical and requests take roughly the same time. HAProxy cycles through servers in order, sending one request to each before looping back to the first. This spreads load evenly if request duration is predictable:
backend web_pool
balance roundrobin
server web1 192.168.1.10:80 check
server web2 192.168.1.11:80 check
server web3 192.168.1.12:80 check
Switch to balance leastconn for variable request durations, such as file uploads, WebSocket connections or API calls with unpredictable processing time. HAProxy tracks active connections per server and sends the next request to the one with the fewest open connections. This prevents one slow request from starving a server while others sit idle. For example, if web1 has 50 active connections and web2 has 30, the next request goes to web2.
Use balance source for simple session affinity without cookies. HAProxy hashes the client IP and always sends the same IP to the same backend, as long as the server is up. This works for applications that store session data locally but breaks when clients are behind NAT or a corporate proxy, since many users share one IP. Add hash-type consistent to minimize disruption when you add or remove servers:
backend session_pool
balance source
hash-type consistent
server web1 192.168.1.10:80 check
server web2 192.168.1.11:80 check
Without consistent hashing, adding a fourth server redistributes roughly 25% of sessions; with it, only about 8% move. For more advanced routing strategies, see HAProxy Load Balancing: Complete Setup Guide for Linux.
Sticky Sessions with Cookies
For session affinity that works across NAT and proxies, use cookie-based stickiness. HAProxy inserts a cookie in the first response, then reads it on subsequent requests to route the client back to the same server:
backend web_pool
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
server web3 192.168.1.12:80 check cookie web3
The insert keyword tells HAProxy to add a Set-Cookie: SERVERID=web1 header in responses. The indirect flag means HAProxy strips the cookie before forwarding the request to the backend, so your application never sees it. The nocache flag adds Cache-Control: private to prevent proxies from caching responses with session cookies. Each server gets a unique cookie value (here web1, web2, web3) and HAProxy uses that value to route returning clients.
To verify this works, open your browser's DevTools Network tab and look at the response headers for the first request. You should see Set-Cookie: SERVERID=web1; path=/. On the second request, check the request headers for Cookie: SERVERID=web1. HAProxy reads this cookie and sends the request to the same backend. If a server goes down, HAProxy ignores the cookie and picks a healthy server, then updates the cookie in the response to reflect the new assignment.
Logging and Monitoring
HAProxy logs to syslog by default, but the default configuration sends everything to /var/log/syslog mixed with other services. For production, separate HAProxy logs into their own file and enable HTTP-specific fields that show response times and status codes. You also need a stats socket for automation and optionally a web UI for quick checks.
Enabling HTTP Logging
In the global section, set log /dev/log local0 to send logs to the local syslog daemon. In defaults or the frontend, add option httplog to enable detailed HTTP logging instead of the minimal TCP format:
global
log /dev/log local0
defaults
log global
option httplog
A typical HTTP log line looks like this:
Jan 15 14:23:45 haproxy[1234]: 203.0.113.42:54321 [15/Jan/2025:14:23:45.123] http_front web_pool/web2 0/0/1/23/24 200 1456 - - ---- 3/3/0/1/0 0/0 "GET /api/users HTTP/1.1"
Reading left to right: client IP and port, timestamp, frontend name, backend and server name, five timing values in milliseconds (Tq/Tw/Tc/Tr/Tt), HTTP status, bytes read, captured request and response cookies (empty here), termination state, connection counters, and the HTTP request line. The timing values matter most: Tq is time in queue, Tc is time to connect to the backend, Tr is time to receive the response, and Tt is total session time. If Tc is high, backends are slow to accept connections; if Tr is high, the application is slow to respond.
Configure rsyslog to write HAProxy logs separately. Create /etc/rsyslog.d/haproxy.conf with:
$ModLoad imudp
$UDPServerRun 514
local0.* /var/log/haproxy.log
& stop
Restart rsyslog with systemctl restart rsyslog. New HAProxy logs appear in /var/log/haproxy.log instead of syslog. The & stop line prevents rsyslog from also writing these messages to /var/log/syslog. On systems using journald, you can skip rsyslog and query logs with journalctl -u haproxy -f to follow in real time.
Stats Socket and Stats Page
The stats socket lets you query runtime state and disable servers without reloading the config. In the global section, add:
global
stats socket /run/haproxy/admin.sock mode 660 level admin
Set ownership with chown haproxy:haproxy /run/haproxy/admin.sock after HAProxy starts, or add your user to the haproxy group with usermod -aG haproxy youruser. Query the socket with socat or nc. To see overall process stats:
echo 'show info' | socat stdio /run/haproxy/admin.sock
This prints uptime, current connections, request rate and memory usage. For per-backend statistics:
echo 'show stat' | socat stdio /run/haproxy/admin.sock
The output is CSV with columns for backend name, server name, queue length, session rate, bytes in/out, response times and check status. Parse this in scripts to feed monitoring systems. To disable a server without reloading, run echo 'disable server web_pool/web1' | socat stdio /run/haproxy/admin.sock. The server stays in the config but HAProxy stops sending it traffic until you run enable server web_pool/web1.
For a web UI, add a stats page to the frontend:
frontend http_front
bind *:8080
stats uri /haproxy?stats
stats auth admin:SecurePass123
Visit http://yourserver:8080/haproxy?stats and log in with the username and password. The page shows all frontends and backends, current connection counts, queue depth, health check status and historical request rates. Refresh the page to see live updates. The stats socket is better for automation because it returns structured data, but the web UI is faster for manual troubleshooting.
For external monitoring, run the Prometheus haproxy_exporter and point it at the stats socket. The exporter scrapes HAProxy metrics every few seconds and exposes them on /metrics for Prometheus to collect. This integrates HAProxy into the same monitoring stack as your application and infrastructure metrics.
Reloading Without Downtime
HAProxy supports graceful reload: the old process keeps handling active connections while a new process takes over new connections. This lets you apply config changes without dropping a single request. Always validate the new config before reloading, and test with real traffic to confirm the handoff works.
Run haproxy -c -f /etc/haproxy/haproxy.cfg to check syntax. If it reports "Configuration file is valid," reload with systemctl reload haproxy. Behind the scenes, systemd runs haproxy -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -sf $(cat /run/haproxy.pid). The -sf flag sends the old process a signal to finish existing connections gracefully, then exit. New connections go to the new process immediately. Active connections stay on the old process until the client closes them or they time out.
To reload manually without systemd, use the same command:
haproxy -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -sf $(cat /run/haproxy.pid)
The new process reads the config, binds the listening sockets (inheriting them from the old process on Linux 2.6+), and signals the old process to stop accepting new connections. For a few seconds, both processes run. You can see this with ps aux | grep haproxy during the reload. Once all old connections close, the old process exits. On a busy server, this can take minutes if clients hold long-lived connections like WebSockets or HTTP/2 streams.
Warning: SSL session cache is lost on reload. Clients with resumed sessions must perform a full handshake after the reload, which adds latency. If you rely on session resumption for performance, schedule reloads during low-traffic windows or use a shared session cache with
ssl-default-server-options.
To verify zero downtime, run a load test with curl in a loop before and during the reload:
while true; do curl -s -o /dev/null -w "%{http_code}\n" https://example.com/; sleep 0.1; done
All responses should be 200. If you see connection errors or timeouts during the reload, check for a bind conflict: two HAProxy processes cannot bind the same IP and port unless socket transfer is working. On older kernels or if HAProxy was built without USE_LINUX_SPLICE=1, you may need to use -st instead of -sf, which terminates old connections immediately instead of waiting for them to close. This causes brief downtime but is sometimes necessary on systems that do not support socket transfer.
Production Readiness Checklist
Before you route production traffic through HAProxy, verify these items. In the global section, confirm maxconn is set high enough for peak load: start with 4096 and increase based on ulimit -n and kernel tuning. Check that ssl-default-bind-ciphers disables weak algorithms and ssl-min-ver TLSv1.2 is set. In each backend, make sure every server has check and reasonable inter, rise, fall values. Test health checks by stopping a backend and confirming HAProxy marks it down within the expected window.
Enable the stats socket and verify you can query it with echo 'show stat' | socat stdio /run/haproxy/admin.sock. Add option httplog and configure rsyslog or journald to capture detailed request logs. Set up log rotation for /var/log/haproxy.log so it does not fill the disk. Test graceful reload under load to confirm no dropped connections. If you use ACLs, walk through every routing rule with sample requests to ensure traffic lands in the correct backend.
For SSL, verify your certificate chain is complete by testing with openssl s_client -connect example.com:443 -showcerts. The output should show your certificate and all intermediates. Check that HSTS headers appear in responses with curl -I https://example.com. If you run multiple HAProxy instances behind a hardware load balancer or DNS round-robin, consider shared session state or avoid sticky sessions entirely. For infrastructure-as-code patterns, Ansible Automation: From Zero to Production covers templating HAProxy configs and automating reloads across a fleet.
Monitor backend response times and error rates. If Tr values in the logs climb above your SLA, investigate slow database queries or external API calls in the application. If Tc is high, backends may be hitting connection limits: add more servers or increase maxconn per server. Use show stat to track queue depth; sustained queuing means you need more backend capacity. For a quick reference during incidents, keep the HAProxy Cheat Sheet open in a browser tab.
Frequently Asked Questions
How do I reload HAProxy without dropping connections?
Run haproxy -c -f /etc/haproxy/haproxy.cfg to validate the new config, then systemctl reload haproxy or manually execute haproxy -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -sf $(cat /run/haproxy.pid). The old process keeps handling active connections while the new process takes over new ones. Test under load with a loop of curl requests to confirm zero dropped connections during the handoff.
What is the difference between roundrobin and leastconn in HAProxy?
balance roundrobin cycles through servers in order, sending one request to each before looping back. It works well for stateless requests with similar durations. balance leastconn sends each request to the server with the fewest active connections, which balances load better when request durations vary widely, such as file uploads or long-running API calls.
How do I configure SSL termination in HAProxy?
Combine your certificate, private key and intermediates into one PEM file with cat cert.pem key.pem intermediate.pem > /etc/haproxy/certs/example.com.pem. In the frontend, add bind *:443 ssl crt /etc/haproxy/certs/example.com.pem. Set strong ciphers in the global section with ssl-default-bind-ciphers and enforce TLS 1.2+ with ssl-default-bind-options ssl-min-ver TLSv1.2. Redirect HTTP to HTTPS with redirect scheme https if !{ ssl_fc }.
How do I check if my HAProxy backends are healthy?
Enable the stats socket in the global section with stats socket /run/haproxy/admin.sock mode 660 level admin, then run echo 'show stat' | socat stdio /run/haproxy/admin.sock. Look at the status column for each server: UP means healthy, DOWN means failed checks. Check check_status for the reason, such as L7OK for HTTP 200 or L7STS for a status mismatch. Review /var/log/haproxy.log for health check failure messages with details on Layer4 or Layer7 errors.
Can HAProxy route traffic based on URL path or domain name?
Yes. Define ACLs in the frontend with acl is_api hdr(host) -i api.example.com for domain matching or acl is_admin path_beg /admin for path prefixes. Then route with use_backend api_pool if is_api or use_backend admin_pool if is_admin. HAProxy evaluates ACLs top to bottom and stops at the first match, so place specific rules before general ones. Combine multiple ACLs on one line for AND logic.