Skip to content
Load Balancing Setup — NGINX, HAProxy, AWS ALB, Health Checks, SSL Termination, and Session Persistence

Load Balancing Setup — NGINX, HAProxy, AWS ALB, Health Checks, SSL Termination, and Session Persistence

DodaTech Updated Jun 20, 2026 11 min read

Load balancing distributes traffic across multiple servers to ensure availability, scalability, and fault tolerance. A single server failure shouldn’t take down your application. This guide covers NGINX upstream load balancing with health checks, HAProxy configuration with a stats dashboard, AWS Application Load Balancer (ALB) with target groups, SSL termination at the load balancer, session persistence (sticky sessions), and active-passive failover strategies.

What You’ll Learn

You’ll configure NGINX as a load balancer with multiple algorithms and health checks, deploy HAProxy with a real-time stats dashboard, set up AWS ALB with auto-scaling target groups, terminate SSL at the load balancer layer, implement sticky sessions for stateful applications, and configure active-passive failover for disaster recovery. Durga Antivirus Pro uses multi-region load balancing to ensure signature updates are always available.

Load Balancing Path

    flowchart LR
  A[Deployment Automation] --> B[Load Balancing<br/>You are here]
  B --> C[NGINX LB]
  B --> D[HAProxy]
  B --> E[AWS ALB]
  C --> F[Health Checks]
  D --> F
  E --> F
  F --> G[SSL Termination]
  G --> H[Session Persistence]
  H --> I[Failover & DR]
  style B fill:#f90,color:#fff
  

NGINX Load Balancer

# /etc/nginx/nginx.conf
# Global upstream definitions
upstream api_servers {
    # Load balancing methods
    # round_robin — default, distributes evenly
    # least_conn — sends to least busy server
    # ip_hash — session stickiness by client IP
    # random — random selection with optional two choices

    least_conn;

    # Server pool with weights
    server 10.0.1.10:3000 weight=5 max_fails=3 fail_timeout=30s;
    server 10.0.2.10:3000 weight=5 max_fails=3 fail_timeout=30s;
    server 10.0.3.10:3000 weight=3 max_fails=3 fail_timeout=30s;

    # Backup server (only used if all others are down)
    server 10.0.4.10:3000 backup;

    # Connection limits per upstream server
    queue 100 timeout=10s;

    # Keepalive connections
    keepalive 32;
    keepalive_requests 1000;
    keepalive_timeout 60s;

    # Active health checks (requires NGINX Plus or nginx-upsync)
    # zone backend 64k;
    # health_check interval=5s fails=3 passes=2 uri=/health;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://api_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;

        # Timeouts
        proxy_connect_timeout 5s;
        proxy_send_timeout 10s;
        proxy_read_timeout 30s;

        # Buffering
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 8k;
    }
}

Testing NGINX Load Balancing

# Check which upstream server handles each request
curl -I http://api.example.com/

# Simulate a server failure
curl -X POST http://10.0.1.10:3000/simulate-failure

# Verify traffic goes to remaining servers
for i in $(seq 1 20); do
    curl -s http://api.example.com/status | grep "server_id"
done

HAProxy

# /etc/haproxy/haproxy.cfg
global
    log /dev/log local0
    maxconn 65535
    user haproxy
    group haproxy
    stats socket /var/run/haproxy.sock mode 660 level admin
    ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256
    ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11

defaults
    log global
    mode http
    option httplog
    option dontlognull
    option forwardfor
    option http-server-close
    retries 3
    timeout connect 5s
    timeout client 30s
    timeout server 30s
    timeout http-request 5s
    timeout queue 10s
    timeout tunnel 3600s     # WebSocket
    default-server inter 5s fall 3 rise 2  # Health checks

# Statistics dashboard
frontend stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 10s
    stats auth admin:CHANGE_ME_PASSWORD
    stats admin if LOCALHOST

# Frontend — incoming traffic
frontend http-in
    bind *:80
    bind *:443 ssl crt /etc/haproxy/certs/example.com.pem

    # Redirect HTTP to HTTPS
    http-request redirect scheme https code 301 unless { ssl_fc }

    # Rate limiting
    stick-table type ip size 100k expire 30s store http_req_rate(10s)
    http-request track-sc0 src
    http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }

    # Security headers
    http-response set-header X-Content-Type-Options nosniff
    http-response set-header X-Frame-Options DENY
    http-response set-header Strict-Transport-Security "max-age=63072000"

    default_backend app_servers

# Backend — application servers
backend app_servers
    balance roundrobin
    option httpchk GET /health HTTP/1.1\r\nHost:\ example.com
    http-check expect status 200

    server app1 10.0.1.10:3000 check weight 5
    server app2 10.0.2.10:3000 check weight 5
    server app3 10.0.3.10:3000 check weight 3

    # Backup server (active-passive)
    server backup 10.0.4.10:3000 check backup

    # Sticky sessions via cookie
    cookie SERVERID insert indirect nocache
    server app1 10.0.1.10:3000 cookie app1 check
    server app2 10.0.2.10:3000 cookie app2 check

HAProxy Administration

# Check status
echo "show info" | socat stdio /var/run/haproxy.sock
echo "show stat" | socat stdio /var/run/haproxy.sock

# Enable/disable servers
echo "disable server app_servers/app1" | socat stdio /var/run/haproxy.sock
echo "enable server app_servers/app1" | socat stdio /var/run/haproxy.sock

# Maintenance mode
echo "set server app_servers/app1 state maint" | socat stdio /var/run/haproxy.sock
echo "set server app_servers/app1 state ready" | socat stdio /var/run/haproxy.sock

# View active connections
echo "show sessions" | socat stdio /var/run/haproxy.sock

# Reload gracefully
haproxy -f /etc/haproxy/haproxy.cfg -p /var/run/haproxy.pid -sf $(cat /var/run/haproxy.pid)

AWS ALB (Application Load Balancer)

# main.tf — Terraform AWS ALB setup
provider "aws" {
  region = "us-east-1"
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
}

resource "aws_lb" "app" {
  name               = "app-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = aws_subnet.public[*].id

  enable_deletion_protection = true
  enable_http2               = true
  idle_timeout               = 60

  tags = {
    Environment = "production"
  }
}

resource "aws_lb_target_group" "app" {
  name        = "app-targets"
  port        = 3000
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  target_type = "ip"

  health_check {
    enabled             = true
    healthy_threshold   = 2
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    path                = "/health"
    port                = "traffic-port"
    protocol            = "HTTP"
    matcher             = "200"
  }

  stickiness {
    type            = "lb_cookie"
    cookie_duration = 86400
    enabled         = true
  }

  tags = {
    Name = "app-target-group"
  }
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.app.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.app.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.app.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

resource "aws_lb_listener_rule" "api" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 100

  condition {
    path_pattern {
      values = ["/api/*"]
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

SSL Termination

# HAProxy SSL termination
frontend https-in
    bind *:443 ssl crt /etc/haproxy/certs/example.com.pem \
        alpn h2,http/1.1
    option httpchk
    http-request set-header X-Forwarded-Proto https if { ssl_fc }

    default_backend app_servers

backend app_servers
    # Backend uses HTTP (SSL terminated at HAProxy)
    server app1 10.0.1.10:3000 check
# NGINX SSL termination
server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://backend;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Session Persistence (Sticky Sessions)

# NGINX ip_hash — session stickiness by client IP
upstream backend {
    ip_hash;
    server 10.0.1.10:3000;
    server 10.0.2.10:3000;
}

# HAProxy cookie-based stickiness (preferred)
backend app_servers
    cookie SERVERID insert indirect nocache
    server app1 10.0.1.10:3000 cookie app1 check
    server app2 10.0.2.10:3000 cookie app2 check

Sticky Session Testing

# Without stickiness — each request may go to a different server
for i in $(seq 1 5); do
    curl -s http://api.example.com/session | grep "server_id"
done
# Output:
# server_id: app1
# server_id: app3
# server_id: app2
# server_id: app1
# server_id: app3

# With stickiness — all requests go to the same server
for i in $(seq 1 5); do
    curl -s -b "SERVERID=app1" http://api.example.com/session | grep "server_id"
done
# Output:
# server_id: app1
# server_id: app1
# server_id: app1
# server_id: app1
# server_id: app1

Active-Passive Failover

#!/bin/bash
# active-passive.sh — Automatic failover between data centers

PRIMARY="10.0.1.10"
SECONDARY="10.0.2.10"
HEALTH_URL="http://$PRIMARY/health"
FAILOVER_SCRIPT="/usr/local/bin/failover.sh"

check_health() {
    curl -sf "$HEALTH_URL" > /dev/null 2>&1
}

perform_failover() {
    echo "$(date): Primary ($PRIMARY) is DOWN. Failing over to $SECONDARY..."

    # Update DNS (Route53 health check)
    aws route53 change-resource-record-sets \
        --hosted-zone-id ZONE_ID \
        --change-batch '{
            "Changes": [{
                "Action": "UPSERT",
                "ResourceRecordSet": {
                    "Name": "api.example.com",
                    "Type": "A",
                    "SetIdentifier": "secondary",
                    "Failover": "PRIMARY",
                    "TTL": 60,
                    "ResourceRecords": [{"Value": "'"$SECONDARY"'"}]
                }
            }]
        }'

    # Update NGINX upstream
    ssh load-balancer "
        sed -i 's/server $PRIMARY:3000;/# server $PRIMARY:3000; # DOWN/' /etc/nginx/conf.d/upstream.conf
        sed -i 's/# server $SECONDARY:3000;/server $SECONDARY:3000;/' /etc/nginx/conf.d/upstream.conf
        nginx -t && systemctl reload nginx
    "

    # Notify
    curl -X POST "https://hooks.slack.com/services/..." \
        -H "Content-Type: application/json" \
        -d '{"text":"⚠ Failover: Primary down, traffic redirected to secondary"}'

    echo "$(date): Failover complete"
}

# Monitoring loop
while true; do
    if ! check_health; then
        perform_failover
        break
    fi
    sleep 10
done

Health Check Strategies

# Health check comparison

NGINX (open source):
- Passive: marks server down after N failures (max_fails)
- No active health checks (NGINX Plus required)
- Slow detection: fail_timeout seconds

HAProxy:
- Active: sends periodic HTTP requests to /health
- Configurable: inter=5s fall=3 rise=2
- Immediate detection and automatic recovery

AWS ALB:
- Active: configurable interval, path, matcher
- Deregistration delay: drain connections before removing
- Cross-zone load balancing: distribute evenly across AZs

Common Errors

1. Sticky Sessions Causing Uneven Load

Users with the same IP hash always hit the same server. If one user has heavy traffic, that server gets overloaded. Use cookie-based stickiness with load-aware distribution or external session stores (Redis).

2. Health Check Not Matching Backend Port

If the health check hits a different port than traffic, it may report unhealthy when the app is fine (or vice versa). Always use port: traffic-port or match the actual application port.

3. SSL Termination Without Proper Forwarding Headers

Backend servers need to know the original protocol. Without X-Forwarded-Proto: https, the app may generate HTTP redirects or mixed content warnings. Always set proxy_set_header X-Forwarded-Proto $scheme;.

4. Connection Draining Not Enabled

When a server is removed from the pool, in-flight requests fail if connections aren’t drained. On AWS ALB, set deregistration_delay.timeout_seconds = 60. On HAProxy, set option http-close or timeout http-keep-alive.

5. Cross-Zone Load Balancing Disabled

Without cross-zone balancing, traffic isn’t evenly distributed across availability zones. If one AZ has more instances, it handles more traffic. Enable cross-zone load balancing on AWS ALB.

6. DNS TTL Too High for Failover

Active-passive failover via DNS requires low TTL. If TTL is 300s, failover takes 5 minutes + client DNS cache. Set TTL to 60s or use a load balancer with instant failover.

7. Single Load Balancer Point of Failure

One load balancer is a single point of failure. Deploy two in different AZs with DNS round-robin or use a cloud load balancer (AWS ALB is managed, multi-AZ by default).

Practice Questions

1. What is the difference between round-robin and least_conn load balancing? Round-robin distributes requests evenly regardless of server load. least_conn sends requests to the server with the fewest active connections, which handles variable-length requests better.

2. How does session persistence work with cookie-based stickiness? The load balancer sets a cookie identifying the backend server. Subsequent requests with that cookie are routed to the same server. The cookie is set once and maintained for the session duration.

3. What is active vs passive health checking? Active: the load balancer periodically sends requests to a health check endpoint. Passive: the load balancer monitors real traffic and marks servers as down after observing failures.

4. Why should SSL termination happen at the load balancer? It offloads CPU-intensive cryptographic operations from application servers. It centralizes certificate management. It allows the load balancer to inspect HTTP traffic for routing decisions.

5. Challenge: Design a multi-region active-active setup with automatic failover. Answer: Deploy application in us-east-1 and eu-west-1. Use Route53 latency-based routing with health checks. Each region has its own ALB + auto-scaling group. Use global DynamoDB or Aurora Global Database for data. On region failure, Route53 routes all traffic to the healthy region.

Mini Project: Multi-Tier Load Balancer Setup

#!/bin/bash
# setup-lb.sh — Set up a two-tier load balancer

echo "=== Two-Tier Load Balancer Setup ==="

# Layer 1: HAProxy (global, SSL termination)
echo "[1/3] Configuring HAProxy..."
cat > /etc/haproxy/haproxy.cfg << 'HAPROXY'
global
    maxconn 65535
    stats socket /var/run/haproxy.sock mode 660

defaults
    mode http
    timeout connect 5s
    timeout client 30s
    timeout server 30s

frontend http-in
    bind *:80
    bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
    http-request redirect scheme https unless { ssl_fc }
    default_backend nginx_lb

backend nginx_lb
    balance roundrobin
    option httpchk GET /health
    server nginx1 127.0.0.1:8081 check
    server nginx2 127.0.0.1:8082 check
HAPROXY

# Layer 2: NGINX (application routing)
echo "[2/3] Configuring NGINX instances..."

for PORT in 8081 8082; do
    cat > /etc/nginx/sites-available/app-$PORT << NGINX
upstream backend {
    server 10.0.1.10:3000;
    server 10.0.2.10:3000;
}
server {
    listen $PORT;
    location / {
        proxy_pass http://backend;
    }
    location /health {
        return 200 "healthy";
    }
}
NGINX
done

# Start services
echo "[3/3] Starting services..."
systemctl restart haproxy
systemctl restart nginx

echo "=== Load balancer setup complete ==="
echo "Traffic flow: User → HAProxy:443 → NGINX:8081/8082 → App:3000"
echo "Stats: http://localhost:8404/stats"

FAQ

What is the best load balancing algorithm?
For most HTTP APIs, round-robin or least_conn works well. For stateful sessions requiring affinity, use ip_hash or cookie-based stickiness. For variable request durations, least_conn distributes more evenly.
Should I use a hardware or software load balancer?
Software (NGINX, HAProxy) is sufficient for most deployments up to very high scale (100K+ requests/second). Hardware (F5, Citrix ADC) offers specialized acceleration but at 10-100× the cost.
Can a load balancer help with DDoS attacks?
Yes — load balancers can absorb traffic up to their capacity, filter malicious patterns, and rate-limit requests. Cloud load balancers (AWS ALB, Cloudflare) have built-in DDoS protection.
What is connection draining?
Connection draining allows in-flight requests to complete before removing a server from the pool. Without it, active requests are terminated when the server is taken offline.
How do I handle WebSocket connections through a load balancer?
Set longer timeouts: timeout tunnel 3600s in HAProxy. For NGINX, set proxy_http_version 1.1 and appropriate proxy headers. AWS ALB supports WebSocket natively.
Do I need a load balancer for a single server?
Not for traffic distribution, but a load balancer provides SSL termination, health checks, and the ability to add servers later without DNS changes. Start with NGINX for simplicity.

What’s Next

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro