Courses / System Design Interview / Chapter 6
Module 2 · Building Blocks · Chapter 6 of 39
Load Balancers
Builds on What System Design Is: From One Server to Millions, Abstractions That Hide the Network, Availability, Reliability, Scalability and Latency and DNS: How a Name Becomes an Address. Next up: Databases: Replication, Partitioning and Choosing a Store.
Part 1 · Why and Where
One address in front of many servers
It is the IPL final, and the innings break has just begun. Orders on a food delivery app jump fivefold in two minutes. One of its twelve API servers runs out of memory and stops answering, but the load balancer in front still thinks it is healthy: it checks each server every 30 seconds and waits for 3 misses in a row. For the next minute and a half, every twelfth order spins and fails, and hungry customers tap "Place order" again and again. Tuned checks would have pulled it out within seconds.
A single server runs out of something eventually: CPU, memory, bandwidth or open connections. More servers fix that but create three new problems. Clients need one stable address, traffic must be spread across the servers, and a dead server must stop receiving requests.
A load balancer solves all three, much like the host at a busy restaurant who seats each arriving group at a free table and stops seating anyone at a broken one. It accepts traffic on one address and forwards each connection or request to one server in a pool behind it, checking the servers' health as it goes. It can be software, an appliance or a managed cloud service. For the food app it buys five things:
- Scale: add servers to the pool as orders grow, and customers never notice.
- Availability: a failed server leaves the rotation automatically, within seconds when the checks are tuned.
- Deploys without downtime: servers restart one at a time while the rest keep serving.
- A smaller attack surface: only the balancer faces the internet.
- One place for shared work: encryption, compression, request logs and basic limits.
Availability, Reliability, Scalability and Latency showed that three servers survive a failure better than one. The load balancer is what turns a pool of machines into one service that keeps answering when a machine dies.
Where load balancers sit
The front door is not the only place a balancer appears. Wherever one tier calls a pool of identical machines, something must choose a machine. A typical order on the food app passes through two balancers:
Read replicas of a database follow the same idea, often balanced by the database driver itself. Choosing between whole regions is a global job, covered later in this chapter.
Between internal services, the caller often does the balancing itself. With client-side load balancing, each service fetches the list of healthy instances from a service registry, one of the discovery options in DNS: How a Name Becomes an Address, and picks an instance per request, with no box in the middle. A service mesh does the same through a small proxy beside each instance. Both save a hop and a box that could fail, at the cost of a library or proxy in every caller.
Before the algorithms, one naming question: is the box in front a load balancer, a reverse proxy or an API gateway?
Load balancer, reverse proxy or API gateway
Three names for the box in front of your servers are often used as if they meant the same thing. They overlap, and one product can play all three roles, but each name puts a different job first:
| Load balancer | Reverse proxy | API gateway | |
|---|---|---|---|
| Main job | Spread traffic over a pool of identical servers and skip dead ones | Stand in for the servers: end encryption, cache, compress, hide their addresses | Be the front door for an API: check callers, apply limits, route to services |
| Works at | L4 or L7 | Usually L7 (HTTP) | L7 (HTTP, gRPC) |
| Knows about | Servers and their health | Requests and responses | API keys, users, quotas and versions |
| Typical rule | Send a third of traffic to each of 3 servers | Serve this image from cache for an hour | Reject calls without a valid token; allow 100 a minute per key |
Load balancerSpread traffic over a pool of identical servers and skip dead ones
Reverse proxyStand in for the servers: end encryption, cache, compress, hide their addresses
API gatewayBe the front door for an API: check callers, apply limits, route to services
Load balancerL4 or L7
Reverse proxyUsually L7 (HTTP)
API gatewayL7 (HTTP, gRPC)
Load balancerServers and their health
Reverse proxyRequests and responses
API gatewayAPI keys, users, quotas and versions
Load balancerSend a third of traffic to each of 3 servers
Reverse proxyServe this image from cache for an hour
API gatewayReject calls without a valid token; allow 100 a minute per key
The lines blur: an L7 load balancer is a reverse proxy that balances, and an API gateway is a reverse proxy with API rules added.
A common layout puts a managed balancer at the edge, an API gateway behind it, and internal balancing in front of each service. On the food app, the gateway rejects calls without a valid login token and caps each restaurant partner's API key at a set rate; Rate Limiters covers how those limits work.
NGINX and HAProxy: balancers and reverse proxies you run yourself. Envoy: a proxy for traffic between services, common in service meshes. Cloud load balancers, L4 and L7: managed fleets, the default in the cloud. Keepalived: moves a shared address between a self-hosted pair. Many API gateways are built on these proxies.
Part 2 · How It Works
L4 and L7
The first real choice is how much of each request the box reads. Balancers are named after the layer of the network model they work at. A Layer 4 (L4) balancer works with TCP and UDP: it sees addresses and ports, picks a server when a connection opens, and passes the bytes through unread, like a sorter who routes letters by the envelope alone. A Layer 7 (L7) balancer speaks the application protocol, usually HTTP, and opens the envelope: it reads each request's path, headers and cookies before choosing. The table compares them:
| L4 (transport) | L7 (application) | |
|---|---|---|
| Sees | IP addresses, ports, connections | URLs, headers, cookies, request bodies |
| Balances | Connections | Individual requests |
| Can route by | Address and port only | Path, host name, header, user |
| Extras | Very fast; works for any TCP or UDP protocol | Decryption, retries, rewrites, compression, caching |
| Cost | Little CPU per byte | More CPU per request, a little added latency |
| Typical use | Raw throughput, non-HTTP traffic, in front of L7 balancers | Web and API traffic, routing to separate services |
L4 is the cheap, fast pipe; L7 spends more CPU per request to route by content.
That routing lets one address front many services. The food app sends /api/orders to the order service, /api/menu to the menu service and /ws to its live order-tracking servers, all behind api.example.in.
An L4 balancer balances connections, not requests. With HTTP/2 or gRPC, one client can keep one connection open and send thousands of requests over it, all to the same server. When a few busy clients hold long connections, a few servers do most of the work. Balance such traffic per request at L7, or cap how long a connection may live so clients reconnect and spread out.
Round robin and health checks
Once the balancer has a pool, it needs a rule for picking a server and a way to notice a dead one. The simplest rule is round robin: send each new request to the next server in the list, and wrap around at the end. It needs no knowledge of the servers and suits identical servers with similar requests, like most of the food app's API calls.
Round robin alone would keep feeding a dead server. Active health checks fix that: the balancer calls a cheap endpoint such as GET /health on every server at a set interval, waits up to a timeout for the reply, removes a server after a number of failures in a row (the unhealthy threshold) and returns it after a number of successes (the healthy threshold). An L4 check may only open a connection; an HTTP check expects 200 OK.
Predict before you watch: a server crashes just after passing a check, and the balancer needs 3 misses in a row. Does traffic to it stop at once?
- No. It keeps its turn in the rotation until the third check fails, so requests sent to it in that gap fail.
Watch for the requests that still reach server 1 after it dies
Read it as steps
Requests rotate across three API servers; checks run every few seconds, and a server is removed after 3 misses in a row.
Request 4 failed although the balancer was checking, because a server leaves the pool only at the unhealthy threshold, and every request sent before then is lost. The length of that gap is a setting you choose, and the next page does the arithmetic.
Health-check timing
Back to the IPL final. The food app's balancer checked every 30 seconds and needed 3 misses, so how long did the dead server keep receiving orders? The gap is roughly the interval times the unhealthy threshold, plus the last check's timeout:
detection ≈ interval × unhealthy threshold + timeout the story: 30 s × 3 + 5 s ≈ 95 s tuned: 5 s × 2 + 2 s ≈ 12 s
Worst case, when the crash lands just after a passed check. Requests lost ≈ detection time × that server's request rate.
At the dinner peak each of the 12 API servers handled about 1,000 requests a second, so the story's 95 seconds cost up to 95,000 failed or hung requests. The tuned setting cuts that to about 12,000.
Why not check every second? Tighter checks fail in their own way. A healthy server that pauses for 3 seconds, say for a garbage collection, misses two quick checks and is thrown out. When many servers pause under the same load, the pool flaps, losing capacity exactly when it is busiest. The healthy threshold is the matching brake on re-entry: requiring 2 or 3 successes stops a server that crashes on every restart from rejoining and failing again.
Passive health checks close most of the remaining gap. The balancer watches real traffic, and when requests to one server keep failing, for example 5 errors or timeouts in a row, it removes the server without waiting for a probe, usually within a second or two at this traffic. Passive checks see only servers that get traffic, so setups use both.
If /health also tests the shared database, one short blip fails every server at once and the balancer removes the whole pool. Check only what the server controls, alert on dependencies separately, and let the balancer stop removing servers once most look unhealthy (failing open).
Least connections
Round robin assumes requests are alike. The food app's restaurant dashboard breaks that: most requests are quick menu edits, but a few are sales-report exports that run for 30 seconds. Round robin hands out work evenly by count, so one unlucky server can collect several exports and fall behind. Least connections sends each new request to the server with the fewest requests in progress, like joining the shortest queue at a ticket counter.
Watch for where each new request goes; badges count requests in progress
Read it as steps
The restaurant dashboard: badges count requests in progress on each server, and one server is stuck on slow exports.
Work in progress is a cheap stand-in for load: a server stuck on slow work keeps its requests open, so it gets less new work, and the balancer needs no knowledge of CPU. An L4 balancer counts open connections; an L7 balancer counts requests in flight, which stays accurate when one connection carries many requests. One catch: a brand-new server shows zero and gets flooded, so balancers ramp its share up gradually, a slow start.
Choosing an algorithm
Round robin and least connections are two of seven common algorithms. Pick the simplest one that fits your traffic, and change it only when you can name the problem it causes:
| Algorithm | How it picks | Use it when | Watch out for |
|---|---|---|---|
| Round robin | Next server in turn | Servers are identical and requests similar | Uneven when some requests take far longer |
| Weighted round robin | In proportion to set weights, such as 2 to 1 | Mixed machine sizes; sending 5% of traffic to a new version | Weights are set by hand and do not follow real load |
| Least connections | Fewest requests in progress | Request times vary: uploads, exports, WebSockets | A new, empty server is flooded without slow start |
| Least response time | Fastest recent replies and fewest in progress | Servers differ in speed, or some share busy machines | Needs measurement, and can swing back and forth |
| IP hash | A hash of the client IP address | Simple stickiness without cookies | Many users share one IP behind mobile carrier NAT; with % N, a pool change remaps most clients |
| Consistent hashing | A hash of a key, such as a user id, on a ring | Keeping each key on the server whose local cache holds it | Needs virtual nodes to stay even |
| Power of two choices | Pick two servers at random, use the less busy one | Many balancers share a pool and would all pick the same least-busy server | Needs a load signal, though only for the two it picked |
How it picksNext server in turn
Use it whenServers are identical and requests similar
Watch out forUneven when some requests take far longer
How it picksIn proportion to set weights, such as 2 to 1
Use it whenMixed machine sizes; sending 5% of traffic to a new version
Watch out forWeights are set by hand and do not follow real load
How it picksFewest requests in progress
Use it whenRequest times vary: uploads, exports, WebSockets
Watch out forA new, empty server is flooded without slow start
How it picksFastest recent replies and fewest in progress
Use it whenServers differ in speed, or some share busy machines
Watch out forNeeds measurement, and can swing back and forth
How it picksA hash of the client IP address
Use it whenSimple stickiness without cookies
Watch out forMany users share one IP behind mobile carrier NAT; with % N, a pool change remaps most clients
How it picksA hash of a key, such as a user id, on a ring
Use it whenKeeping each key on the server whose local cache holds it
Watch out forNeeds virtual nodes to stay even
How it picksPick two servers at random, use the less busy one
Use it whenMany balancers share a pool and would all pick the same least-busy server
Watch out forNeeds a load signal, though only for the two it picked
Most pools run round robin or least connections; the hashing rows matter only when a server holds something local, such as a warm cache.
With plain hashing (hash(key) % N), growing from 4 servers to 5 moves about 80% of keys. A key stays only when hash % 4 equals hash % 5: 4 of every 20 values. Consistent hashing moves about a fifth, the new server's share. Designing a Key-Value Store, later in this course, shows how the ring works.
Part 3 · Design Choices
Sticky sessions, and why stateless is better
The hashing rows raise a bigger question: should one user always reach the same server? Some apps keep a user's session, such as their login or the food app's cart, in the memory of the server that created it, so every later request must return there. Sticky sessions (session affinity) make the balancer do this, usually with a cookie it sets or with IP hashing. The cost shows up when anything changes:
| Problem with sticky sessions | What happens |
|---|---|
| A server dies | Its users lose their sessions: logged out, carts emptied mid-order |
| Uneven load | A few heavy users pinned to one server keep it busy while others sit idle |
| Slow scale-in and deploys | Removing a server means waiting for its sessions to end, or logging its users out |
| New servers help slowly | Existing users stay put; only new users spread out |
Each problem comes from the same cause: the session lives on one machine.
The better fix is to make servers stateless, as Availability, Reliability, Scalability and Latency described: keep sessions in a shared store such as a distributed cache, read in under a millisecond, or in a signed token (for example a JSON Web Token, or JWT, in a cookie) that the client sends with every request. Any server can then handle any request, and the balancer is free to use whichever algorithm suits the traffic.
Affinity is still fine when losing it costs only speed: a user routed to the server whose local cache holds their data is just slower elsewhere if it dies. WebSockets are naturally pinned, since one connection lives on one server.
TLS termination
Every request from the food app arrives encrypted, so some machine must decrypt it. Which one? TLS (Transport Layer Security, the encryption behind HTTPS) costs one round trip on top of the TCP handshake with TLS 1.3, or two with the older TLS 1.2, plus public-key maths on the server. Terminating TLS at the load balancer means the balancer holds the certificate and decrypts there. An L7 balancer must decrypt anyway to read paths and headers, and the handshake maths then runs on the balancer instead of every app server. There are three options:
| Option | How it works | Trade-off |
|---|---|---|
| Terminate at the balancer | Decrypt there; plain HTTP to servers on a private network | Simplest, certificates in one place, but traffic inside is unencrypted |
| Re-encrypt | Decrypt, inspect and route, then open a new encrypted connection to the server | Encrypted on every hop, at more CPU cost; common for payment data |
| Passthrough | An L4 balancer forwards encrypted bytes; servers decrypt | Encrypted end to end, but the balancer cannot route by path or see requests |
The food app terminates at the balancer for menus and tracking, and re-encrypts the payment path, because card and UPI details should never cross even a private network in plain text.
Terminating at the balancer also gives one place to renew certificates and resume sessions, so returning clients skip part of the handshake. HTTP/3 runs over QUIC, on UDP, and merges the transport and TLS handshakes into one round trip; balancers usually end it at the edge and speak HTTP/1.1 or HTTP/2 to the servers behind them.
Global and local load balancing
Everything so far happens inside one region. When the food app runs in Mumbai and Hyderabad, something must also decide which region each customer reaches. A local load balancer spreads traffic across servers in one region; a global load balancer picks the region: the nearest for speed, and a healthy one when a region fails. Two techniques do the global job:
| DNS-based global balancing | Anycast | |
|---|---|---|
| How | The DNS answer depends on the resolver's location, measured latency and region health | Every region announces the same IP address; routers carry each user to the nearest site in routing terms |
| Failover speed | Minutes: cached answers must expire first | Usually faster: a failed region withdraws its routes |
| Granularity | Per resolver | Per network route |
| Good for | Most web apps; simple with a managed DNS service | Large providers, DNS itself, absorbing floods at the edge |
Most teams start with DNS-based balancing and accept its minutes of failover, because anycast needs a large provider's network.
So the layers stack: a customer in Kolkata looks up api.example.in, the global layer picks Mumbai, and Mumbai's local balancer picks a healthy server. If Mumbai fails, the global layer moves new lookups to Hyderabad, while customers holding a cached answer keep trying Mumbai until it expires, as DNS: How a Name Becomes an Address showed. The global layer fails over in minutes; the local layer in seconds.
Part 4 · Load Balancers in Practice
The balancer as a single point of failure
Put one load balancer in front of the food app's twelve servers and you have built a new single point of failure: if that box dies, all twelve are unreachable. The standard fix is a pair of balancers sharing a virtual IP (VIP), an address that can move from one machine to another:
- 1The active balancer owns the VIPclients only ever connect to the VIP
- 2It sends heartbeatsfor example once a second; the passive one listens
- 3The active one stops answeringa crash, a power cut, a network fault
- 4The passive one claims the VIPafter a few missed heartbeats, so a few seconds
- 5It tells the local networka broadcast (a gratuitous ARP) points the network at the new owner
- 6Traffic resumesopen connections may reset, and clients reconnect
A pair can also share the work instead of keeping one idle. The two arrangements compare like this:
| Active-passive | Active-active | |
|---|---|---|
| Normal operation | One serves, one waits | Both serve |
| Hardware use | Half the capacity sits idle | All capacity in use |
| On failure | The passive one takes over the VIP in seconds | The survivor takes all traffic, so each must run below 50% |
| Main risk | Split brain, if both believe they are active | Overload, if the 50% rule is broken |
Either way, the spare capacity is the price of surviving a failure.
VRRP (Virtual Router Redundancy Protocol) is the standard protocol for this takeover, and very large sites put a layer of L4 balancers in front of a fleet of L7 ones. Managed cloud balancers are already fleets across zones; you still spread them, and the servers behind them, over more than one zone.
Draining: deploys without dropped requests
A redundant balancer protects you from its own failure. Deploys are the failure you cause yourself: a new release of the food app restarts 12 servers one at a time, and restarting a server mid-request fails those requests. Connection draining lets a server leave gracefully: no new requests, and time for the current ones to finish.
The order of the steps matters, because balancers learn slowly that a server is leaving: a check must fail, or a deregistration call must reach every balancer node. The full shutdown sequence:
- 1The deploy tool asks the server to stopfor example with a stop signal (SIGTERM)
- 2The server fails its readiness check but keeps servingthe signal to balancers: send nothing new
- 3Wait until every balancer has noticeda few check intervals, often 5 to 15 s
- 4Finish requests in flightclose idle keep-alive connections; HTTP/2 sends GOAWAY
- 5Exit when done, or at the drain limitfor example 30 s; long uploads need more
- 6Start the new version and rejoinslow start ramps its share up over a minute or so
The step teams skip is the third. A server that stops accepting connections the moment it gets the stop signal is still on the balancers' lists for several seconds, so they keep sending it requests, which are refused, and customers see 502 Bad Gateway errors on every deploy. Keep serving until the balancers have stopped sending, then stop.
Two kinds of check keep this clean. A readiness check answers "may I get traffic?" and a liveness check answers "should I be restarted?"; draining fails only the first. A liveness check must never test shared dependencies, or a database blip restarts every server at once.
WebSockets and reconnect storms
Draining works when requests finish in seconds. The food app's live order tracking does not: each WebSocket stays open for the whole order, on the one server it first reached. One connection server can hold about 100,000 mostly idle sockets, so restarting it disconnects 100,000 customers at the same moment.
They all reconnect at once. If they return within 10 seconds, that is 10,000 new connections a second, each with a TCP and TLS handshake, a login check and a sync of missed rider updates. This reconnect storm hits the balancer, the login service and the remaining servers, once per server in a rolling deploy, and leaves each restarted server empty while the others hold its old clients. Four habits tame it:
- Drain slowly. Close a server's sockets in batches over a few minutes, or ask clients to reconnect after a random delay.
- Back off with jitter. Each client waits a random, growing delay before retrying, such as up to 1 s, then 2 s, then 4 s, so retries spread out instead of arriving in waves.
- Balance new sockets by least connections, so reconnecting clients fill the emptiest servers, including the one just restarted.
- Keep headroom for the burst. Size the login service for the reconnect rate, and let returning clients resume their TLS session to skip part of the handshake.
Retries at the balancer
One more tool covers the gaps that checks and draining leave: the balancer can retry a failed request on another server. It hides failures well, and it can also cause harm.
A retry is safe when the first attempt never reached a server, such as a refused connection, or when the request is safe to repeat, such as GET /api/menu. It is not safe for POST /orders: the first server may have placed the order just before timing out, so a retry places it twice. That is the double-action problem Abstractions That Hide the Network solved with idempotency keys. Let the balancer retry only safe methods, or requests that carry an idempotency key.
Retries also multiply load. During an overload every failed request becomes two, so a struggling pool gets more work just when it can least cope: the retry storm from Availability, Reliability, Scalability and Latency. Retry at most once, within a budget such as 10% extra requests, and fail fast beyond it.
The balancer sees every request, which makes it the best place to watch the whole pool:
In the interview
Draw a load balancer wherever a pool of identical servers takes traffic, and say which layer, which algorithm and what happens when something fails.
- Say
- "An L7 balancer terminates TLS and sends /ws to its own pool, so 100,000 reconnecting tracking sockets land on the WebSocket servers, not the API servers."
- Say
- "Checks every 5 s, out after 2 misses, plus passive removal on errors: a dead server leaves in seconds, not the 95 s that slow defaults allow."
- Say
- "Servers are stateless, with sessions in a shared cache at under 1 ms a read, so any server takes any request and a deploy drains each in 30 s."
- Avoid
- "The load balancer handles scaling." It spreads load over the servers you have; adding servers is autoscaling's job, and a slow database stays slow.
- Avoid
- "Retry every failed request." A retried POST can place an order twice; retry only safe or idempotent requests, within a budget of about 10% extra.
Wrap-up
| Term | Meaning |
|---|---|
| L4 balancer | Picks a server per TCP or UDP connection, without reading the bytes |
| L7 balancer | Reads HTTP paths, headers and cookies, and routes each request |
| Health check | A regular probe that takes a failing server out of the pool |
| Connection draining | No new requests to a server while its current ones finish |
| Sticky session | Every request from one user goes to the same server |
| TLS termination | The balancer decrypts traffic, so the servers behind it need not |
| Virtual IP | An address that moves to the standby balancer if the active one fails |
| API gateway | A reverse proxy that adds API rules: callers, quotas, versions |
Next, Databases: Replication, Partitioning and Choosing a Store. Stateless servers work because the state moved into the database, and a database cannot simply be copied behind a load balancer. That chapter shows how to copy it and split it safely.
Part 5 · Check yourself
Quick check
Q1Why should the order servers' /health endpoint avoid checking the shared orders database, even though they cannot work without it?
Think it through, then check.
- A few seconds of database trouble fails every check at once, the balancer removes the whole pool, and a blip becomes an outage.
- Check what the server controls, alert on the database separately, and let the balancer fail open.
Q2After internal calls moved to gRPC behind an L4 balancer, two of eight order servers run at 90% CPU while the rest sit near 20%. What is the likely cause, and what are two fixes?
Think it through, then check.
- gRPC sends many requests over one long-lived HTTP/2 connection, and L4 balances connections: a few busy callers landed on two servers and stayed.
- Balance per request at L7 or in the gRPC client; or cap connection lifetime at a few minutes so callers reconnect and spread out.
Q3A release restarts the food app's 12 API servers one at a time. Each takes 15 s to leave the pool, up to 30 s to drain and 45 s to start and warm up. How long does the deploy take, and how much spare capacity does the pool need?
Think it through, then check.
- Up to 15 + 30 + 45 = 90 s per server, so about 18 minutes for all 12.
- With one out, 11 servers each carry 12 ÷ 11 ≈ 109% of their usual share, so run below about 90% at peak.
- Two at a time takes 9 minutes but needs each server below about 83%.
Q4One pool serves quick menu edits and 30-second report exports. Do you switch it to least connections, or give exports their own pool? State the trade-off.
Think it through, then check.
- Least connections is a one-line change that stops exports piling onto one server, but a burst of exports can still slow every edit.
- A separate export pool keeps edits fast at month-end, at the cost of a second pool to run and size. Heavy, frequent exports justify it.
Q5From Abstractions That Hide the Network: the balancer retries a timed-out POST /orders on another server, and some customers are charged twice. Why, and what fixes it?
Think it through, then check.
- The first server may have placed the order just before the timeout, so the retry ran it a second time.
- Send an idempotency key with each order, so the server answers a repeat with the stored result. Until then, retry only safe methods.