What is Load Balancer?

A load balancer evenly distributes incoming traffic among web servers or workers defined in a load-balanced pool. Users connect to the public IP / VIP of the load balancer; backend servers are reachable over private IP from the load balancer only. Examples: Nginx, HAProxy, Amazon ELB/ALB/NLB, F5, Cloudflare.

Block Diagram — Load Balancer Placement

The client never talks to individual web servers directly. DNS resolves the domain to the load balancer's virtual IP (VIP). The LB terminates or forwards the connection, picks a healthy backend from its pool, and rewrites the destination address before forwarding on the private network.

flowchart TB
    subgraph Internet
      C[Client / Browser
DNS: example.com → 1.2.3.4] end C -->|HTTP/TCP to VIP 1.2.3.4| LB subgraph DC["Data Center / VPC — private network"] LB[Load Balancer
VIP: 1.2.3.4 public
10.0.0.1 private] subgraph Pool["Backend pool"] WS1[Web Server 1
10.0.1.10:8080] WS2[Web Server 2
10.0.1.11:8080] end LB -->|forward to chosen backend| WS1 LB -->|forward to chosen backend| WS2 WS1 --> Cache[(Cache cluster)] WS2 --> Cache Cache --> DB[(DB farm)] end

Advantages of LB

1. Failover

If Web-Server1 goes offline, all the traffic will be routed to Web-Server2. This prevents the website from going offline. If traffic grows rapidly and two servers are not enough, new web servers can be added to the pool without changing the public VIP the clients use.

Places where LB can be kept

Place Description
Between Client (browser) and Web server LB sends each request to a free / least-loaded web server.
Between App-servers and cache Hundreds of cache servers may need load balancing.
Between cache and DB servers Read replicas or sharded DB nodes can sit behind an LB.

Scheduling Algorithms for LB

Name Description
1. Round Robin Send request one after the other in rotation.
2. Least Connections Sends requests to the server with the lowest number of active connections.
3. Least time Sends requests to the server selected by a formula combining fastest response time and fewest active connections.
4. Hash Distributes requests based on a key you define, such as client IP or request URL.
5. IP HASH (HTTP only) Distributes requests based on the first three octets of the client IP address.
6. Random with 2 Choices Picks two servers at random and sends the request to the one with fewer active connections.
7. DNS Round Robin with Load Balancing Assign multiple IPs (IP1, IP2, IP3) to one domain. DNS responds with one IP in rotation; each returned IP moves to the end of the list so traffic is spread evenly.
8. Sub domain DNS Delegation with Round Robin Multiple subdomains (hr.example.com, payroll.example.com) have their own nameservers; the primary nameserver forwards queries to the correct subdomain NS.
9. Client side random load balancing Deliver a list of server IPs to the client; the client randomly picks one per connection. Assumes all clients generate similar load.
10. Server side load balancers LB listens on a port, forwards the request to all backend servers, and whichever responds first serves the client.
11. IP ADDRESS based If the client IP is in range x–y, forward to backend-1; if in range a–c, forward to backend-2, and so on.
12. Layer 5 Aware Inspects Layer-5 session data (e.g. HTTP headers) to decide which backend receives the packet.

Types of LB

Type Description
ALB / Layer-7 / Application LB / Reverse Proxy LB inspects Layer-7 application data (HTTP/HTTPS headers, URL path, cookies) and routes based on that content.
Layer 4 LB / Transport Layer LB Operates at the transport layer (TCP, UDP, TLS). Packet is routed based on source/destination IP and ports without parsing application payload.

Problem: Small Sized HTTP Response via LB
Issue: Incoming request passes via LB and response is also sent via LB. If the response is small (2 MB) and the LB is serving larger requests, this small response has to wait unnecessarily.
Solution — DSR (Direct Server Return): backends answer directly to the clients without passing the response back through the LB.
Layer 3 LB / NLB (Network LB) / VPN LB Routing decision is made from network-layer headers (source and destination IP). See the dedicated Layer 3 section below.
IPVS (IP Virtual Server) Linux kernel feature that acts as a Layer-4 load balancer — a built-in tool within the Linux kernel for high-speed packet forwarding.

Layer 3 Load Balancer (NLB, VPN concentrators, anycast edge routers)

A Layer-3 (network-layer) load balancer makes routing decisions using IP headers only. It does not inspect TCP ports in depth (that is Layer 4) and never parses HTTP (Layer 7). It is commonly used for NLB, VPN concentrators, and anycast edge routers that steer flows before transport termination.

How L3 Load Balancer Routes Traffic (Decision Path)

Consider an HTTP request arriving at load balancer with two web servers(WS1, WS2) behind it. The LB must pick WS1 or WS2 in well under a microsecond. LB does not query a database on every request — it keeps hot-path state in memory.
At starting LB keeps on checking health of Web servers and maintains the state of Web servers

struct BackendServer {
    int backendId;
    string ip;          // Real IP of web server
    int port;           // Real Port of web server
    int weight;
    int maxConnections;
    string availabilityZone;
    vector <string> tags;
    enum class HealthState {UP, DOWN, DRAIN} health_state;
};
unordered_map <int, BackendServer> backendPool;

Steps:
1. Client completes TCP handshake with the LB VIP (SYN → SYN-ACK → ACK). LB stores every HTTP Client information in Connection Table.

struct FiveTuple {    //
    string srcIP;                   // Src IP of HTTPS Client which connects to LB
    string dstIP;                   // Dst IP to which HTTPS Client want to connect. LB IP
    uint16_t srcPort;               // Src Port
    uint16_t dstPort;               // Dst Port
    uint8_t protocol;
    bool operator==(const FiveTuple& other) const {
        return srcIP == other.srcIP &&
               dstIP == other.dstIP &&
               srcPort == other.srcPort &&
               dstPort == other.dstPort &&
               protocol == other.protocol;
    }
};
struct ConnectionState {
    int backendId;
    string translatedIP;
    uint16_t translatedPort;
    chrono::steady_clock::time_point lastAccess;
};
unordered_map <FiveTuple, ConnectionState> connectionTable;

HashTable
            Key                       |   Value       
 src=1.1.1.1,dst=LBIP,                |   backendId=1           // WS1=1, WS2=2
 sport=2012,dport=2323,               |   translatedIP=2.2.2.2  
 Protocol=TCP/UDP                     |   translatedPort=5000
                                      |   lastAccess=
            

2. TLS termination (if HTTPS): LB decrypts using session keys cached in an SSL session cache
3. LB opens (or reuses from a connection pool) a TCP connection to WS2 10.0.1.11:8080 and forwards the HTTP request.
4. Response travels back through the LB to the client; connection table entry is updated or removed when the connection closes.

Why modern load balancers are fast

No disk on the hot path — every lookup is RAM-only.
Kernel bypass — DPDK, XDP/eBPF, or hardware ASICs process packets in user space or NIC firmware, avoiding syscalls per packet.
Pre-warmed backend connections — HTTP keep-alive and TLS session resumption remove repeated handshakes.
Health checks off the request path — probe results are cached; the forwarding thread only reads a boolean flag.

Capacity — Hardware Specs and Load Metrics

Profile Hardware / form factor Typical sustained load
Small software LB (1 vCPU VM, nginx/HAProxy) 1 vCPU, 2 GB RAM, 1 Gbps NIC ~5k–20k req/s (small HTTP), ~10k concurrent connections
Mid-tier software LB (8 vCPU, DPDK) 8 vCPU, 16 GB RAM, 10 Gbps NIC ~100k–500k L4 connections/s, ~1–3 M concurrent flows
Hardware ADC (F5, A10 class) Dual CPU, 32–64 GB RAM, FPGA/ASIC assist, 40–100 Gbps ~1–10 M concurrent connections, multi‑million L7 req/s with SSL offload
Cloud NLB / ALB (managed) Hyperscaler auto-scaled data plane (opaque) Scales to millions of req/s; limits published per account/region (e.g. ALB pre-warming for sudden spikes)

Load metrics to watch

Metric What it tells you
Requests or connections per second Headroom vs licensed / tested maximum
Concurrent connections Memory pressure on connection table (each flow ≈ hundreds of bytes–KB)
P99 latency at LB Queueing inside LB when CPU or NIC is saturated
P95 means 95% of requests complete at or below this time
P99 means 99% of requests complete faster
CPU utilization per core TLS and L7 parsing are CPU-heavy; imbalance suggests sharding needed
NIC bandwidth (Gbps) and PPS Small-packet DDoS or microservice chatter can hit PPS limit before bandwidth cap
SSL handshakes per second Often the first bottleneck unless session resumption is high
Backend healthy count Traffic concentrates on fewer nodes when health checks fail

Scaling the Load Balancer Itself

Important distinction: scaling backend web servers (adding WS3, WS4 to the pool) is separate from scaling the load balancer tier. When LB capacity is exhausted, you do not magically spawn a new LB instance from a single hardware box — you add redundancy and capacity through architecture.

What gets provisioned when load grows?

Deployment style How LB capacity is added Pod vs standalone?
Software LB (nginx, HAProxy, Envoy) on Kubernetes Horizontal Pod Autoscaler increases replica count of the LB Deployment; each pod shares the VIP via kube-proxy, MetalLB, or an external cloud LB. New pods in the same cluster — not new physical machines unless the cluster itself scales nodes.
Software LB on VMs Add another VM running the same LB software; place both behind ECMP, DNS round-robin, or anycast VIP. New standalone VM provisioned (second LB instance).
Hardware ADC (F5, Citrix) Add a second physical appliance in active/standby or active/active cluster; license and sync config. New standalone machine — dedicated appliance, not a pod.
Cloud managed (AWS ALB/NLB, GCP LB) Provider auto-scales opaque data-plane nodes; you may request pre-warming for sharp spikes. No pod you manage. Internal scaling is hidden; you do not create pods yourself.

Backends scale out independently: auto-scaling groups or Kubernetes HPA add application pods/VMs and register them in the LB's backend pool via service discovery (Consul, Kubernetes Endpoints, cloud target groups). The LB configuration picks up new backends from its pool table — it does not "birth" them itself.

Primary and Secondary Load Balancer — Communication

Production systems almost never run a single LB. Two (or more) instances share a Virtual IP (VIP) or anycast address so clients see one entry point while operators get redundancy.

Active / Standby (Primary + Secondary)

flowchart LR
    C[Clients] --> VIP[VIP 1.2.3.4]

    VIP --> P[Primary LB
ACTIVE
holds VIP via VRRP] VIP -.->|VIP floats on failover| S[Secondary LB
STANDBY
syncs config] P --> B1[Backend pool] S -.->|takes over on failure| B1 P <-->|VRRP/keepalived heartbeats
config sync| S

Active / Active

Both LBs serve traffic simultaneously. Clients reach either node via:

Each node maintains its own connection table. Stickiness must use a deterministic algorithm (source-IP hash or cookie) so the same client tends to hit the same node, or stickiness is abandoned in favor of stateless backends.

Session Stickiness (Persistence)

Session stickiness forces requests from the same client (or session) to reach the same backend across multiple requests, even when the LB would otherwise round-robin.

When stickiness helps

Scenario Why stickiness matters
Shopping cart / in-memory session Server stores cart in local RAM; without stickiness the cart appears empty on another node.
WebSocket or SSE long-lived connection Connection is pinned to one backend for its lifetime anyway; stickiness ensures reconnect lands on a node that can resume (or use shared store).
Legacy apps without shared session store Avoids rewriting the app to use Redis for sessions.
File upload chunked across requests Chunks must land on the same worker holding partial state.

When stickiness hurts or is unnecessary

Internal logic — common mechanisms

Method How it works internally
Insert cookie (LB-generated) On first response LB sets Set-Cookie: SERVERID=ws2; HttpOnly. On later requests LB reads the cookie, looks up ws2 in the backend table, bypasses round-robin. Stored in stickiness hash map with TTL.
Application cookie (passive) LB hashes an existing cookie (e.g. JSESSIONID) with consistent hashing → backend id. No new cookie needed; works if the app already sets one.
Source IP hash backend = hash(client_ip) mod N. Stateless, fast, but poor distribution behind NAT.
TLS session ID / ticket Hash of SSL session id maps to the node that terminated the first handshake — mainly for reconnect optimization.
Header-based Route by X-User-Id or tenant id hash — useful in multi-tenant SaaS.

Stickiness + multiple LB nodes

In active/active, an inserted cookie must be honored by all LB instances (same cookie name → same backend mapping algorithm). If only the primary knows the table, failover breaks sessions. Best practice: make backends stateless with a shared session store (Redis) and treat stickiness as an optimization, not correctness.