HAProxy Best Practices: Load Balancing and Failover, the Right Way
HAProxy is one of the most widely deployed load balancers on the internet — fast, flexible, and well-suited to environments that outgrew a single server. Here are the production best practices that hold up over time: health checks, fallback backends, TCP-mode balancing for FTP and databases, graceful reloads, and when to reach for something other than HAProxy.
HAProxy sits in front of a remarkable amount of internet infrastructure. It's the load-balancer-and-reverse-proxy of choice for everyone from small SaaS teams running two backend nodes to hyperscale operators routing millions of requests per second. The reasons are consistent: it's fast, the configuration syntax is comprehensible, the failure modes are observable, and the project has been maintained continuously since 2001 without ever feeling abandoned. This post walks through the best practices that hold up in production — health checks that catch real failures, fallback patterns for graceful degradation, balancing non-HTTP services like FTP, database health checks, and the operational habits that keep HAProxy boring.
Best practice 1: Use Layer 7 health checks, not just TCP checks
The simplest health check tells HAProxy to TCP-connect to the backend port; if the connection opens, the backend is considered healthy:
backend my_app_be
balance roundrobin
option tcp-check
server node1 10.1.1.10:8080 check
server node2 10.1.1.11:8080 check
This catches "server is completely down" but misses "process is up but wedged" — the application accepts a TCP connection but can't actually serve a real request. The better pattern is an HTTP health check against a dedicated /health endpoint:
backend my_app_be
balance roundrobin
option httpchk HEAD /health
http-check expect status 200
server node1 10.1.1.10:8080 check
server node2 10.1.1.11:8080 check
This requests /health from each backend and only counts a 200 OK response as healthy. The /health endpoint should check the application's actual dependencies — database connectivity, downstream service reachability, queue depth — and return non-200 when any of them are broken. A health check that just returns 200 unconditionally is barely better than a TCP check.
Best practice 2: Configure fallback backends for graceful degradation
What happens when every primary backend is unhealthy? Without explicit configuration, HAProxy returns a 503 "no available server" error directly to the client. That's correct but inhospitable. The fix is a backup backend:
backend my_app_be
balance roundrobin
option httpchk HEAD /health
server node1 10.1.1.10:8080 check
server node2 10.1.1.11:8080 check
server backup1 10.1.1.100:8080 check backup
The backup keyword marks a server as a fallback — HAProxy only routes traffic to it when no non-backup servers are healthy. Use the backup server to serve a static maintenance page, a read-only copy of the application, or an explicit error page that explains the situation. Anything is better than a raw 503.
For partial degradation (most backends down but not all), HAProxy can split traffic between primary and shedding backends using ACLs and use_backend rules. That's the production pattern for "we're overloaded; route 90% of users to the slow path and queue the rest."
Best practice 3: Use TCP mode for non-HTTP services
HAProxy is most often deployed in front of HTTP, but it works just as well as a Layer 4 load balancer for any TCP service. The configuration switches to mode tcp and the health check changes to match the protocol of whatever's behind it.
For an FTP backend cluster, the canonical pattern is:
backend ftp_be
mode tcp
balance roundrobin
option tcp-check
tcp-check expect string "ProFTPD"
server ftp1 10.1.1.10:21 check
server ftp2 10.1.1.11:21 check
The tcp-check expect string directive tells HAProxy to open a TCP connection, read the server's banner, and only count the backend as healthy if the banner contains the expected string. For FTP this catches the failure mode where the process is up but the FTP daemon itself is wedged.
The same pattern works for SMTP (expect 220 ), Redis (expect +PONG), and most other line-protocol services. SSH and SFTP are harder because the banner is encrypted; for those, TCP-mode balancing without banner inspection is the practical compromise.
Best practice 4: Database health checks need protocol awareness
Load-balancing database connections is tricky because TCP-level checks miss the most common database failure modes. HAProxy ships with protocol-aware health checks for MySQL, PostgreSQL, and a few others:
backend mysql_be
mode tcp
balance leastconn
option mysql-check user haproxy-health
server mysql1 10.1.1.10:3306 check
server mysql2 10.1.1.11:3306 check
option mysql-check connects as the specified user, performs a login handshake, and only counts the backend as healthy if the database accepts the login. Create the haproxy-health user with no permissions beyond the ability to connect — it exists purely for the health check.
balance leastconn instead of roundrobin makes more sense for databases: database connections are typically long-lived and unevenly weighted, and least-connections distributes load better than blind rotation.
For PostgreSQL, the equivalent is option pgsql-check. For Redis, option tcp-check with a send PING / expect string +PONG pattern. Database read replicas can be load-balanced with HAProxy; the primary almost never should be — promotion needs a controller that knows the cluster's failover semantics (Orchestrator, repmgr, Patroni), not a load balancer making routing decisions.
Best practice 5: Plan for graceful reloads from the start
HAProxy's reload command spawns a new process with the new configuration, then drains the old one. In a busy production environment, this works smoothly only if you've configured for it.
The two settings that matter most:
global
nbthread 4
maxconn 100000
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
defaults
timeout client 60s
timeout server 60s
timeout connect 5s
option redispatch
stats socket with expose-fd listeners lets the new HAProxy process inherit the listening sockets from the old one — zero-downtime reloads instead of brief connection refusal during the handoff. option redispatch retries a request on a different backend if the chosen one fails between the initial dispatch and the actual connection.
Most distros ship a systemctl reload haproxy target that handles this correctly; verify on staging before assuming it does in production.
Best practice 6: Layer 7 routing for multi-app deployments
The "one HAProxy in front of many apps" pattern is what most teams converge on — a single front door with ACL-based routing to per-app backends:
frontend public
bind *:443 ssl crt /etc/haproxy/certs/
acl host_api hdr(host) -i api.example.com
acl host_web hdr(host) -i www.example.com
acl path_admin path_beg /admin/
use_backend api_be if host_api
use_backend admin_be if host_web path_admin
default_backend web_be
This routes based on the Host header and URL path. ACLs can also test source IP, query parameters, cookies, header values, and TLS SNI — most production routing patterns are expressible in three to five ACL lines.
The discipline that prevents this from getting unmanageable: name ACLs after what they match, not what they trigger. acl host_api hdr(host) -i api.example.com is readable in three months; acl route_to_api requires reading the rest of the rule to understand.
Best practice 7: Monitor what HAProxy can see
HAProxy ships with a stats endpoint that exposes per-backend health, connection counts, request rates, error rates, and queue depth. Expose it on an admin interface (not the public frontend):
frontend stats
bind 127.0.0.1:8404
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:strong-password-here
Scrape it with Prometheus via haproxy_exporter, or pull the CSV format directly. The metrics worth alerting on:
- Backend availability — at least one healthy server per backend at all times.
- Connection error rate — a sudden rise in
econ(connection errors) usually means a backend is hard-down. - Queue depth —
qcurrising indicates backends can't keep up with the request rate. - HTTP response codes —
hrsp_5xxspiking means an application-layer problem the load balancer can't fix.
The native HAProxy log line is rich. Pipe it through structured logging (rsyslog into Loki, or directly via syslog) and the per-request data becomes queryable.
When HAProxy isn't the right tool anymore
Three failure modes push teams to look for something else:
- Service-mesh routing in Kubernetes. Once a workload is running on Kubernetes with multi-tenant ingress, services, and mTLS between pods, the HAProxy-in-front-of-VMs model fits less naturally. The Kubernetes ecosystem has its own ingress controllers (NGINX Ingress, Traefik, Istio gateway, HAProxy Kubernetes Ingress Controller if you want to stay in the family) that integrate with the cluster's service-discovery layer.
- Global load balancing across regions. HAProxy is a single-instance load balancer; routing traffic across geographic regions usually wants a DNS-based or anycast-based layer (Cloudflare Load Balancing, AWS Route 53 weighted routing, AWS Global Accelerator) above HAProxy rather than HAProxy itself.
- Application-layer features beyond routing. WAF, bot management, full TLS analytics, request transformation pipelines — these typically live in a managed CDN/edge layer (Cloudflare, Fastly, AWS CloudFront) rather than in HAProxy configs that grow to thousands of lines.
For most teams running ten-to-a-few-hundred backend servers with relatively static topology, HAProxy is still the right tool. It only starts feeling under-powered at the edges where the operational model shifts.
A note for teams running HAProxy in front of FTP / SFTP clusters
If you're operating HAProxy in front of self-hosted FTP, SFTP, or FTPS servers, the architecture works — the TCP-mode balancing patterns in this post handle it cleanly. What it doesn't solve is the operational surface: patching the underlying servers, key/credential management, audit logging across the cluster, certificate rotation, and the per-protocol firewall complexity.
A hosted managed-file-transfer platform handles all of that at the platform layer. Files.com exposes FTP, FTPS, SFTP, and WebDAV on a single subdomain with HA built in — no HAProxy required, no per-protocol cluster to maintain. For teams whose HAProxy config is mostly "load-balance our SFTP nodes," moving the file-transfer layer to Files.com lets the load-balancer surface shrink to whatever else actually needs balancing.
For the narrow set of teams that must run file-transfer infrastructure inside their own datacenter, the free ExaVault on-premise appliance handles the same protocols from a self-hosted VM image — and pairs cleanly with HAProxy as the front door when you need multiple appliance instances behind a single endpoint.
FAQ
What's the difference between HAProxy and NGINX as a load balancer?
NGINX started as a web server and grew load-balancing features; HAProxy started as a load balancer and grew TCP-mode and Layer-7 ACLs. In 2026 they overlap heavily for most workloads. HAProxy still has the edge on raw TCP throughput and on observability (the stats endpoint is genuinely better); NGINX has the edge when you also want to serve static content or run an embedded scripting layer (Lua / OpenResty). For pure load balancing, either works.
What load-balancing algorithms does HAProxy support?
The main ones: roundrobin (the default, rotates evenly), static-rr (similar but doesn't reweight on the fly), leastconn (sends to the backend with the fewest active connections — best for long-lived connections like databases), source (hashes the client IP for sticky routing), uri (hashes the URL path — useful for cache-aware routing), url_param (hashes a specific query parameter), and hdr (hashes a specific header value). roundrobin is the right default for most HTTP workloads; leastconn for databases and other long-connection services.
Can HAProxy load-balance HTTPS traffic?
Yes, two ways. TLS pass-through (mode tcp with no TLS configuration) forwards the encrypted bytes to the backend without inspecting them — useful when the backends terminate TLS themselves. TLS termination (mode http with bind *:443 ssl crt /path/to/cert.pem) decrypts at HAProxy and re-encrypts or passes plaintext to the backend — useful when you want HAProxy to do Layer-7 routing on the decrypted request.
How does HAProxy handle failover?
Two layers. Backend failover — health checks remove unhealthy backends from rotation automatically, with optional backup servers stepping in when all primary backends fail. HAProxy process failover — running multiple HAProxy instances behind a floating IP (managed by keepalived, Pacemaker, or a cloud load balancer) handles the case where HAProxy itself goes down. In cloud environments, the managed Layer-4 load balancer (AWS NLB, GCP TCP/UDP LB) usually plays the role keepalived plays in self-hosted environments.
Is HAProxy free?
Yes. The open-source HAProxy community edition is free under the GPL. The company HAProxy Technologies sells a commercial HAProxy Enterprise edition with support, additional modules, and management tooling. For most use cases the community edition is what's actually running in production, including at very large scale.
What's a load balancer vs a reverse proxy?
In practice, the same software does both. A load balancer routes traffic across multiple backends for scale and redundancy. A reverse proxy sits in front of one or more backends to add functionality (TLS termination, caching, request transformation, authentication) — even if there's only one backend. HAProxy and NGINX are both load balancers and reverse proxies; the terms describe what the software is doing in a given deployment, not different products.