Skip to content
Logo
Table of contents
VeryCloud
DocumentationDedicated servers

HAProxy: load balancer to distribute traffic

HAProxy: load balancer to distribute traffic

Install HAProxy to distribute traffic between multiple backend servers, manage healthchecks and terminate SSL. Reference open-source tool used by Github, Twitter, Stack Overflow for millions of connections.

Category
Dedicated servers
Level
Advanced
Reading time
6 min
Published
May 17, 2026

Introduction

When a single server is no longer enough to handle load, you move to load balancing: a front proxy distributes requests to multiple backends. HAProxy is the open-source reference load balancer.

Use cases:

  • Website → 3 Nginx backend servers, auto failover if one dies
  • API → round-robin distribution with sticky sessions
  • Database → MySQL/PostgreSQL read replica load balancing
  • Centralized SSL termination + advanced healthchecks

Prerequisites

  • Linux VPS dedicated to the load balancer (Debian 12 / Ubuntu 22.04+)
  • At least 2 backend servers already in place
  • A domain name (optional but recommended)

Step 1: Installation

sudo apt update
sudo apt install -y haproxy
sudo systemctl enable --now haproxy
sudo systemctl status haproxy

Recommended version: HAProxy 2.8+ (LTS).

Step 2: Target architecture

                    ┌──────────────┐
   Clients ────────▶│   HAProxy    │
                    │  (LB VPS)    │
                    └──────┬───────┘
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌─────────┐  ┌─────────┐  ┌─────────┐
        │ Web 1   │  │ Web 2   │  │ Web 3   │
        │ 10.0.0.10│ │10.0.0.11│  │10.0.0.12│
        └─────────┘  └─────────┘  └─────────┘

Backends communicate privately (ideally via private network between VPS, or via WireGuard).

Step 3: Minimal config (HTTP)

sudo nano /etc/haproxy/haproxy.cfg
global
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon
    maxconn 50000

    # Modern SSL
    ssl-default-bind-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets

defaults
    log global
    mode http
    option httplog
    option dontlognull
    option http-keep-alive
    option forwardfor
    timeout connect 5s
    timeout client 60s
    timeout server 60s
    timeout http-request 10s
    timeout http-keep-alive 15s
    maxconn 50000

# Internal stats UI
listen stats
    bind 127.0.0.1:8404
    stats enable
    stats uri /
    stats refresh 10s
    stats auth admin:AStrongPassword

# Frontend
frontend http_front
    bind *:80
    default_backend web_servers

# Backend
backend web_servers
    balance roundrobin
    option httpchk GET /health
    http-check expect status 200
    server web1 10.0.0.10:80 check inter 2000 rise 2 fall 3
    server web2 10.0.0.11:80 check inter 2000 rise 2 fall 3
    server web3 10.0.0.12:80 check inter 2000 rise 2 fall 3

Important details:

  • balance roundrobin: fair distribution. Others: leastconn (fewest active connections), source (sticky by IP)
  • option httpchk GET /health: healthcheck on /health (your app must respond 200)
  • check inter 2000 rise 2 fall 3: check every 2s, considered up after 2 successes, down after 3 failures

Validate config:

sudo haproxy -c -f /etc/haproxy/haproxy.cfg

Reload:

sudo systemctl reload haproxy

Step 4: Implement healthcheck endpoint on backend

On each backend server, add a /health endpoint returning 200 if app is healthy:

Nginx

server {
    location = /health {
        access_log off;
        return 200 "OK";
        add_header Content-Type text/plain;
    }
}

Node.js

app.get('/health', (req, res) => {
    res.status(200).send('OK');
});

More advanced: also check MySQL and Redis in the healthcheck to avoid routing to a backend with a dead DB.

Step 5: Stats UI

Open http://127.0.0.1:8404 via SSH tunnel:

ssh -L 8404:127.0.0.1:8404 user@HAPROXY_VPS_IP

Then http://localhost:8404 in your browser. Credentials: admin / AStrongPassword.

You see:

  • Backend status (UP/DOWN, green/red)
  • Per-server statistics
  • Real-time load

Step 6: Add SSL (termination)

HAProxy can terminate SSL and talk HTTP to backends (common architecture).

Prepare the certificate

With Let's Encrypt:

sudo apt install -y certbot
sudo systemctl stop haproxy
sudo certbot certonly --standalone -d your-domain.com
sudo systemctl start haproxy

Combine cert + key into a single file for HAProxy:

sudo bash -c "cat /etc/letsencrypt/live/your-domain.com/fullchain.pem /etc/letsencrypt/live/your-domain.com/privkey.pem > /etc/ssl/haproxy.pem"
sudo chmod 600 /etc/ssl/haproxy.pem

Edit the config

frontend https_front
    bind *:80
    bind *:443 ssl crt /etc/ssl/haproxy.pem alpn h2,http/1.1
    
    # Force HTTPS
    http-request redirect scheme https unless { ssl_fc }
    
    # HSTS
    http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
    
    default_backend web_servers

Reload:

sudo systemctl reload haproxy

Step 7: Auto SSL renewal with hook

So Let's Encrypt renews without breaking HAProxy:

sudo nano /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
#!/bin/bash
DOMAIN=your-domain.com
cat /etc/letsencrypt/live/${DOMAIN}/fullchain.pem /etc/letsencrypt/live/${DOMAIN}/privkey.pem > /etc/ssl/haproxy.pem
chmod 600 /etc/ssl/haproxy.pem
systemctl reload haproxy
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh

Step 8: Load balancing algorithms

Based on your case:

backend web_servers
    # Round-robin (default)
    balance roundrobin
    
    # Least connections
    balance leastconn
    
    # Source IP hashing (natural sticky session)
    balance source
    
    # URI hashing (cache-friendly)
    balance uri
    
    # Cookie-based sticky session
    cookie SERVERID insert indirect nocache
    server web1 10.0.0.10:80 cookie web1 check
    server web2 10.0.0.11:80 cookie web2 check

Step 9: ACLs (advanced routing)

Route by URL, header or IP:

frontend https_front
    bind *:443 ssl crt /etc/ssl/haproxy.pem
    
    # Path routing
    acl is_api path_beg /api
    acl is_admin path_beg /admin
    
    # Hostname routing
    acl host_blog hdr(host) -i blog.your-domain.com
    
    # IP routing (limit /admin)
    acl admin_whitelist src 82.26.157.10
    
    # Apply
    use_backend api_servers if is_api
    use_backend admin_servers if is_admin admin_whitelist
    use_backend blog_servers if host_blog
    default_backend web_servers

Step 10: TCP load balancing (MySQL, Redis, SSH)

HAProxy can also load-balance TCP protocols (mode tcp):

frontend mysql_front
    bind *:3306
    mode tcp
    default_backend mysql_servers

backend mysql_servers
    mode tcp
    balance leastconn
    option mysql-check user haproxy_check
    server db1 10.0.0.20:3306 check
    server db2 10.0.0.21:3306 check backup  # backup = used if db1 down

Step 11: Limit connections (anti-flood)

frontend https_front
    bind *:443 ssl crt /etc/ssl/haproxy.pem
    
    # Stick table (per-IP counter)
    stick-table type ipv6 size 100k expire 30s store conn_rate(10s),http_req_rate(10s)
    
    # Limits
    http-request track-sc0 src
    http-request reject if { sc_conn_rate(0) gt 50 }      # > 50 connections/10s = block
    http-request reject if { sc_http_req_rate(0) gt 200 } # > 200 req/10s = block

Step 12: Logs and monitoring

# Live logs
sudo tail -f /var/log/haproxy.log

# Errors only
sudo grep -i error /var/log/haproxy.log

# Top backends by requests
sudo awk '{print $7}' /var/log/haproxy.log | sort | uniq -c | sort -rn | head -10

For continuous monitoring: see the Prometheus + Grafana tutorial with haproxy_exporter.

Troubleshooting

"no backend available"

All your backends are marked DOWN. Check healthchecks:

curl http://10.0.0.10/health

Must return 200 OK.

"503 Service Unavailable"

Either no backend available, or server timeout too short. Increase timeout server.

SSL handshake errors

The .pem file is malformed. Regenerate:

cat fullchain.pem privkey.pem > haproxy.pem

Order matters: chain then key.

Stats UI not accessible

Check port 8404 isn't blocked by UFW. Prefer SSH tunneling over exposing it.

Useful commands

# Test config
sudo haproxy -c -f /etc/haproxy/haproxy.cfg

# Reload (zero downtime)
sudo systemctl reload haproxy

# View active sockets
sudo socat /run/haproxy/admin.sock - <<< "show info"

# Disable a server (maintenance)
sudo socat /run/haproxy/admin.sock - <<< "disable server web_servers/web1"

# Re-enable
sudo socat /run/haproxy/admin.sock - <<< "enable server web_servers/web1"

# View backend states
sudo socat /run/haproxy/admin.sock - <<< "show servers state"

# Reset stats
sudo socat /run/haproxy/admin.sock - <<< "clear counters"

Conclusion

With HAProxy in front:

  • Your infra tolerates a backend failure (auto failover)
  • You distribute load across N servers
  • You centralize SSL termination
  • You protect against application-level flood

Going further:

  • Set up two HAProxy in HA with keepalived (virtual IP failover)
  • Use HAProxy Data Plane API for automated driving
  • Migrate to Traefik if you want native Docker/Kubernetes service discovery

Resources

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members