Load Balancing Setup — NGINX, HAProxy, AWS ALB, Health Checks, SSL Termination, and Session Persistence
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"
doneHAProxy
# /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 checkHAProxy 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 checkSticky 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: app1Active-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
doneHealth 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 AZsCommon 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’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