Load Balancing
Spread requests over many servers and route around dead ones: layer 4 vs 7, algorithms, health checks, sticky sessions and a balancer that stays up.
On this page
Launch day: the API expects 3,000 requests a second at peak, and one app server handles about 1,000 before its latency climbs. So you need at least three servers. But clients know one address, and when one of those servers crashes mid-afternoon, somebody has to stop sending it traffic.
The idea
A load balancer sits in front of many identical servers and owns the public address; it picks a server for each request or connection. That gives you two things at once: capacity, because you scale by adding servers behind it, and availability, because a dead server is taken out of the pool.
It costs an extra hop (under a millisecond inside a data center), one more component that must not fail, and a rule: any server must be able to handle any request. That rule is what "stateless" means.
Spreading requests and surviving a crash
Run four servers, not three, so losing one still leaves enough. Each then gets 3,000 ÷ 4 = 750 requests a second. Step through round robin, then a crash.
- next
- B
- rps per server
- 750
1Request 1 arrives. Round robin hands requests to servers in turn, so it goes to A.
- next
- A
- rps per server
- 750
2Requests 2, 3 and 4 go to B, C and D. Each server gets 750 of the 3,000 a second.
- next
- A
- rps per server
- 750
3Every 5 seconds the balancer calls
GET /healthon each server. All four answer 200, so all stay in the pool.- next
- D
- rps per server
- 750
4Server C's process crashes. The balancer does not know yet, and the next request for C gets
connection refused.- next
- A
- rps per server
- 750
5The request never reached a server, so the balancer can safely retry it on D. A request that timed out after reaching C is different: retry it only if it is idempotent.
- next
- A
- rps per server
- 750
6C fails two health checks in a row and is marked down, at most about 10 seconds after the crash. Without retries, a quarter of requests would fail in that window: up to 750 × 10 = 7,500 errors.
- next
- B
- rps per server
- 1000
7Now A, B and D share the load: 3,000 ÷ 3 = 1,000 each, exactly their limit. The fourth server was not waste; it was what kept the site up.
Health checks come in two kinds. Active checks call an endpoint on a timer, as above. Passive checks watch real traffic and eject a server that returns errors or times out, which catches problems faster.
Layer 4 or layer 7
| Option | Good at | Bad at | Pick it when |
|---|---|---|---|
| Layer 7 (HTTP) Usual default | Reads each request: routes by path or header, terminates TLS, retries, balances each request separately even when they share a connection. | More CPU and a little more latency per request. Only works for protocols it understands. | Web apps and APIs. The usual answer. |
| Layer 4 (TCP/UDP) | Forwards connections by IP and port without reading them. Very fast, works for any protocol. | Cannot route by URL or retry a single request. A long-lived connection stays on one server, however busy it gets. | Non-HTTP traffic, huge connection counts, or when TLS must pass through untouched. |
Choosing a server
| Option | Good at | Bad at | Pick it when |
|---|---|---|---|
| Round robin Usual default | Simple and even when requests cost about the same. Weighted round robin gives bigger servers more. | Ignores load: a server stuck on slow requests keeps getting its full share. | Short, similar requests. The default. |
| Least connections | Sends work to the server with the fewest open connections, so slow requests do not pile up. | Needs per-server counts. A new empty server can be flooded at first. | Request times vary a lot, or connections are long-lived (WebSockets). |
| Hash of a key | The same user or key always lands on the same server, which keeps its local cache warm. | Hot keys make hot servers. Adding a server moves keys unless you use consistent hashing. | Per-key locality matters: cache shards, a user's in-memory state. |
Hashing is where consistent hashing comes in: with a plain hash % n, going from 4 to 5 servers moves about 4 keys in 5 to a different server.
Sticky sessions and stateless servers
If server A keeps your shopping cart in its own memory, every request from you must reach A. Balancers can do that with sticky sessions, usually a cookie naming the server. But load gets uneven, and when A dies the carts on it are gone.
The better answer is stateless servers: keep session data in a shared store (an in-memory store such as Redis) or in a signed token the client sends. Then any server can handle any request, a crash loses nothing, and you add or remove servers freely.
When the balancer is the weak point
One balancer in front of everything is a single point of failure. The usual fix is the pair in the first diagram: an active and a standby that share a virtual IP, with the standby taking the IP when heartbeats stop. At larger scale, run several active balancers and spread clients over them with DNS returning several addresses, or with one address announced from many places (anycast). Managed cloud balancers already do this; say so rather than drawing it.
Reverse proxies and API gateways sit in the same spot. A reverse proxy receives requests on behalf of servers: it terminates TLS, compresses and caches. A layer 7 load balancer is a reverse proxy that spreads load. An API gateway is a reverse proxy with API concerns added: authentication, rate limiting, and routing /orders and /users to different services. In a diagram, one box labelled "API gateway / load balancer" is fine.
Numbers worth remembering
- Worst-case detection time is check interval × failures needed: 5 s × 2 = about 10 s.
- Servers needed: peak requests ÷ per-server capacity, rounded up, plus at least one spare.
How many app servers do you run so the peak is still served after one server fails?
- 2,000 requests a second on average
- Peak is 3 times the average
- One server handles 500 requests a second at a healthy load
Show the working
- Peak: 2,000 × 3 = 6,000 requests a second
- Servers at peak: 6,000 ÷ 500 = 12
- One spare for a failure: 12 + 1 = 13
Estimate 13 (anything from 6 to 40 is a good answer)
With several zones, size so losing a whole zone still leaves enough: more spare than one server.
In the interview
Draw a load balancer as soon as you have more than one server, then name the choices it implies: "Layer 7, round robin, health checks every few seconds, and the app servers are stateless, with sessions in a shared store, so I can scale them freely."
Users keep getting logged out. Why might that be?
We could turn on sticky sessions so each user stays on one server.
Users keep getting logged out. Why might that be?
Sounds like sessions live in each server's memory and the balancer sends the next request elsewhere. I'd move sessions to a shared store, so any server can read them and a server restart loses nothing. Stickiness can stay as a cache-warmth hint, not as the place the data lives.
Follow-ups to expect:
- "What if the load balancer dies?" Active-standby pair sharing a virtual IP, or several active ones behind DNS or anycast.
- "How do you deploy without dropping requests?" Take a server out of the pool, let in-flight requests finish (draining), update it, put it back.
- "WebSockets behind a balancer?" Least connections, since each connection lasts hours, and plan for clients reconnecting when a server goes away.
Common mistakes
Health checks run every 10 seconds and a server is marked down after 3 failures in a row. It crashes while taking 400 requests a second, and the balancer does not retry. About how many requests fail?
A chat service keeps WebSocket connections open for hours, and some users are much more active than others. Which balancing algorithm fits?
You need to route /api/orders to one service and /api/users to another. What do you need?
What to practise next
Once requests reach your servers, the next win is not doing the same work twice: Caching. To see a balancer in front of separate read and write paths, work through Design a URL Shortener.