Load Balancing
A load balancer is the single entry point in front of a pool of servers that distributes traffic, runs health checks and fails over dead instances. Layer-4 balances TCP connections; layer-7 understands HTTP and routes by URL, header or host. Choosing the right algorithm and health check is what separates an even, resilient pool from a stack that collapses on one hot server.
What is this?
A load balancer sits in front of a group of servers and presents a single virtual IP or DNS name to clients. It distributes each connection or request to a backend, removes unhealthy servers from rotation via health checks, and gives you safe rollouts through connection draining. Layer-4 balancers work on IP and TCP port; layer-7 balancers parse HTTP and can route by path, host or header, terminate TLS and rewrite requests.
Why do you need it?
- Without balancing, one server absorbs traffic, saturates, and the tail latency of the whole site degrades.
- Health checks let the pool self-heal: a crashing server is ejected before clients ever see a 500.
- Deployments become safe: drain connections on the old version while new instances warm up, then cut over.
- SSL termination at the edge offloads cryptography from app servers and centralizes certificate management.
- Sticky routing (hashing) keeps a user on one server for the duration of a request or session when needed.
Mental model
Airport check-in with one long queue
A supervisor sending each passenger to the shortest desk
The supervisor (load balancer) keeps every agent busy, reassigns customers when an agent calls in sick (health check), and stops sending people to a closed desk (failover). Without the supervisor, everyone funnels to the nearest desk and that line grows forever.
How it works
- Clients resolve one virtual IP or hostname; all traffic lands on the load balancer, not individual servers.
- L4 balancers forward packets or TCP connections based on destination IP and port without inspecting payloads.
- L7 balancers terminate the connection, inspect the HTTP request (path, host, headers) and proxy it to a chosen backend.
- An algorithm — round-robin, least-connections, IP hash, consistent hash — picks the target for each unit of work.
- Health checks (TCP, HTTP or scripted) run on an interval and take unhealthy servers out of the active set.
- During deployments, connection draining lets in-flight requests finish before an instance is removed.
Traffic flows through one virtual IP, is picked by an algorithm, and dead servers are pruned by health checks.
Key commands
docker compose up -d --scale api=3Start a three-instance backend pool to balance across.
Container api-1, api-2, api-3 Started.
nginx -tValidate the nginx configuration file before reloading.
nginx: configuration file /etc/nginx/nginx.conf test is successful
nginx -s reloadApply the new upstream configuration without dropping connections.
for i in $(seq 1 20); do curl -s http://localhost:8080/; echo; doneHit the balancer repeatedly and watch the instance identifier alternate between backends.
ab -n 5000 -c 100 http://localhost:8080/Load-test the balanced pool and compare throughput with the single-instance run.
Requests per second: 2140.44 [#/sec] (mean)
docker logs nginx-lb --since 5mInspect the balancer access and error logs for upstream failures.
Examples
NGINX layer-7 upstream with least-connections and failover
# nginx.conf - HTTP load balancerupstream backend { least_conn; server api-1.internal:8080 max_fails=3 fail_timeout=30s; server api-2.internal:8080 max_fails=3 fail_timeout=30s; server api-3.internal:8080 backup;} server { listen 80; location / { proxy_pass http://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_next_upstream error timeout http_502 http_503; proxy_connect_timeout 2s; }}The upstream block names the backend pool. least_conn routes each request to the server with the fewest active connections, which suits long-lived or uneven requests. max_fails and fail_timeout mark a server down after three failures in 30 seconds, and proxy_next_upstream retries on the next server if one errors. The third server is a backup, used only when both primary servers are down.
In production
- Cloud load balancers (AWS ALB/ELB, Google Cloud LB) front every public service, terminating TLS and splitting traffic across availability zones so a zone failure does not take the service down.
- Database read replicas sit behind an L4 balancer so analytics and reporting queries spread across replicas while writes keep going to the leader.
- Global DNS-based balancing (GSLB) routes users to the nearest region; within each region, an L7 balancer splits traffic by URL pattern.
- Service meshes and gRPC systems balance at the connection level with round-robin over a resolver, adding retries and circuit breaking at the client side.
Best practices
- Health checks should probe the application, not just the TCP port: return 200 only when the app can actually serve requests.
- Prefer least-connections (or consistent hashing) over plain round-robin when requests have very different durations.
- Run the balancer as an active/passive or active/active pair — a single load balancer is a single point of failure.
- Enable connection draining and disable stickiness during deploys so in-flight requests finish on the old version.
- Pass X-Forwarded-For and the scheme header so backends see the real client IP and preserve HTTPS redirects.
Common mistakes
- Using sticky sessions as a default, which pins users to instances and defeats horizontal scaling.
- Round-robin across heterogeneous instances, overloading small instances and underusing large ones.
- Shallow health checks — a health endpoint that returns 200 even when the database is down.
- Adding servers to the pool without draining old ones, dropping in-flight requests during every deploy.
Common issues
502 Bad Gateway on every requestAll upstream servers are failing health checks, or the upstream server block is empty.
Check each backend's health endpoint, nginx error log, and nginx -t; mark the servers reachable before reloading.
All traffic lands on one backend despite round-robinClient-side keepalive or IP-hash routing pins connections to one server, or the other servers are marked down.
Remove sticky/hash config, confirm the other backends pass health checks, and re-run the distribution test.
High latency at the balancer even though backends are idleNo keepalive to upstreams, so every request opens a fresh TCP handshake, or the balancer is in a remote region.
Enable proxy_http_version 1.1 and a keepalive connection pool to upstreams; place the balancer near clients and backends.
Debugging tips
- Read the balancer access log: it records which backend handled each request, exposing a single hot server.
- Bypass the balancer with curl --resolve example.com:80:backend-ip to test one backend directly.
- Request repeatedly and check the distribution of backend identifiers to verify the algorithm is actually spreading load.
- Compare total time to upstream_time in logs: a large gap means queueing at the balancer or the backend.
Interview checkpoint
Hands-on lab
- Run a three-instance Node API pool that reports its identity
- Configure NGINX as an L7 load balancer in front of it
- Verify requests are distributed and observe failover
- Apply least-connections and health checks to harden the pool
Balance Traffic Across a Node Pool with NGINX
40 min · intermediate
Quick quiz
Which load-balancing layer can route requests by URL path or Host header?
A plain TCP health check is always sufficient to know whether a backend is healthy.
Users must land on the same backend server for the duration of a single session. Which routing mechanism is most appropriate?
Cheat sheet
Round-robinEven sequential distribution; ignores loadLeast-connectionsRoute to fewest active connectionsIP hashSame client -> same server; loses affinity on changesConsistent hashingStable mapping with minimal reshufflingWeightedProportional to instance size/capacityupstream backend { server a; server b; }Define the poolleast_conn;Choose the algorithmserver x max_fails=3 fail_timeout=30s;Failover on repeated errorsproxy_next_upstream error timeout http_502;Retry another server on failureproxy_set_header X-Real-IP $remote_addr;Pass the real client IP