VPS Load Balancing with Nginx and HAProxy: Distributing Traffic Across Multiple Servers

Every VPS has a ceiling. A single instance can serve a few thousand requests per second while idle, but the moment you add a database, background workers, and TLS termination to the same box, CPU contention and connection limits start producing timeouts during traffic spikes. Load balancing fixes this by spreading incoming traffic across two or more servers so no single machine becomes a bottleneck — or a single point of failure. This guide walks through production-grade load balancing on VPS hardware using Nginx and HAProxy, both of which run comfortably on a 2 GB instance.

Before choosing a topology, decide how many servers your budget allows. The minimum viable setup is one load balancer in front of two application servers, with the database on its own machine or a managed service. If you are still picking hardware, our comparison table lists providers with the CPU allocation, network throughput, and bandwidth that matter most for multi-server architectures.

Layer 4 vs Layer 7: Which Balancing Mode Do You Need?

Load balancers operate at two levels. Layer 4 balancing forwards raw TCP/UDP traffic based on IP and port — it is fast, protocol-agnostic, and ideal for databases, WebSockets, or any non-HTTP service. Layer 7 balancing inspects HTTP headers, cookies, and paths, which enables smarter routing (for example, sending /api/* to one backend and everything else to another) at the cost of a bit more CPU. For most web workloads, Layer 7 is the right default; use Layer 4 for anything that is not HTTP.

ModeScopeBest forTrade-off
Layer 4 (TCP/UDP)IP + portDatabases, WebSockets, non-HTTP servicesNo smart routing, no header inspection
Layer 7 (HTTP/HTTPS)Headers, paths, cookiesWeb apps, API gateways, TLS terminationHigher CPU per connection

Option A: Nginx as a Layer 7 Load Balancer

If you already run Nginx on the balancer node, you do not need another daemon — the same binary can proxy and balance. Define an upstream block and point a server block at it:

http {
    upstream app_backend {
        least_conn;               # route to the least-loaded backend
        server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
        server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;
    }

    server {
        listen 80;
        location / {
            proxy_pass http://app_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

Validate with nginx -t and reload with systemctl reload nginx. The least_conn directive sends each request to the backend with the fewest active connections — a better default than round-robin when request processing times are uneven. The max_fails and fail_timeout parameters mark a backend down after three failed requests within 30 seconds.

Option B: HAProxy for Mixed L4/L7 Traffic

HAProxy is purpose-built for load balancing and handles both modes in one process. A frontend/backend pair for HTTP balancing looks like this:

frontend http_in
    bind *:80
    default_backend web_servers

backend web_servers
    balance roundrobin
    option httpchk GET /healthz
    server web1 10.0.0.11:8080 check inter 5s fall 3 rise 2
    server web2 10.0.0.12:8080 check inter 5s fall 3 rise 2

The httpchk directive makes HAProxy probe /healthz on each backend every 5 seconds and remove any server that fails three consecutive checks. Point that endpoint at a lightweight handler that verifies the database connection is alive — this catches half-dead backends that still accept TCP connections but return 500s. Run systemctl enable --now haproxy after installing the package and checking the config with haproxy -c -f /etc/haproxy/haproxy.cfg.

Session Stickiness and TLS Termination

Stateless applications can be balanced freely; stateful ones need stickiness. Nginx supports ip_hash; inside the upstream block to pin a client to one backend, while HAProxy can insert a cookie SRV insert indirect nocache so the chosen backend is remembered via a cookie. For HTTPS, terminate TLS on the balancer and pass plain HTTP to the backends over a private network — this concentrates certificate handling in one place and keeps backend configs simple. Use the provider’s private network interface for backend traffic so plaintext never crosses the public internet.

Verifying the Setup

  • Balancer stats: HAProxy exposes http://<balancer-ip>:8404/stats once you add stats enable to the config; Nginx provides stub_status for active-connection counts.
  • Distribution: run tail -f /var/log/nginx/access.log on each backend and confirm requests alternate between them.
  • Failover test: stop one backend with systemctl stop nginx and confirm the balancer stops sending it traffic within one health-check interval, then recovers it when you start it again.
  • Balancer headroom: ss -s on the balancer shows socket counts so you can spot connection exhaustion before it becomes a user-facing problem.

Start with two backends and a single balancer, then scale horizontally as traffic grows. Load balancing also unlocks zero-downtime deploys: update backends one at a time while the balancer keeps serving the other. For the hardware behind this setup, see the full specs and pricing across providers before you commit.

If you need full root access to run HAProxy or Nginx with custom configs, InterServer’s unmanaged VPS plans include a dedicated IP and flat-rate pricing that keep multi-server budgets predictable.

Leave a Reply