System designCore9 min
Load balancing
Six servers, one address: how a load balancer decides which server gets each request, notices when one dies, and lets you add or remove servers without anyone noticing. And why the obvious way to share work is not the best one.
1 · The job
One address in front, many servers behind
A food delivery app gets 3,000 requests a second at lunchtime. One server cannot keep up, so you run six. But the app on every phone knows only one address, api.food.example. Something has to stand at that address and hand each request to one of the six.
That something is a load balancer. It has four jobs:
- Spread the work so no server is swamped while another idles.
- Route around failure: stop sending requests to a server that has died.
- Hide change: add a seventh server, or take one out for an upgrade, without any client knowing.
- Be one stable address for the outside world, however the servers behind it change.
Clients know one address. Behind it, the balancer picks a server for every connection or request.
It only works if any server can answer any request. A server that keeps a user's cart in its own memory forces the balancer to send that user back to the same server every time. Keep that state in a shared store instead, and the servers become interchangeable (see Scalability).
2 · L4 or L7
A layer 4 balancer moves connections; a layer 7 balancer moves requests
Balancers come in two kinds, named after the network layer they read (see How data moves across a network). The difference decides what each can do.
Aspect | Layer 4 (TCP) | Layer 7 (HTTP) |
|---|---|---|
Reads | Addresses and ports only | The whole request: path, headers, cookies |
Decides per | Connection | Request |
Can route by path or user | No | Yes: |
Handles TLS | Passes encrypted bytes through | Usually decrypts it, so servers don't have to |
Cost | Very cheap, millions of connections | More work per request |
A layer 4 balancer picks a server when a connection opens, by hashing its addresses and ports, and every byte on that connection then goes to that server until it closes. That is a problem for modern clients. HTTP/2 and gRPC send hundreds of requests down one long-lived connection, so a layer 4 balancer pins all of them to whichever server it picked first. A layer 7 balancer reads each request and spreads them, even when they share a connection.
Reading the path lets one address serve two pools: cheap menu reads and slower order writes scale separately.
3 · Picking a server
Round robin is fair to servers, not to requests
The obvious rule is round robin: server 1, then 2, then 3, and round again. Each server gets the same number of requests. With weighted round robin, a server twice as big gets twice as many.
It would be perfect if every request cost the same. They don't. Fetching the menu takes 5 ms; building a big order history can take 500 ms. In large real systems the most expensive requests cost a thousand times the cheapest. Round robin hands the next request to server 3 even while server 3 is still grinding through three slow ones, and server 4 sits idle.
Better rules look at how busy each server is right now:
- Least outstanding requests: send to the server with the fewest requests in progress. It needs an up-to-date count for every server.
- Power of two choices: pick two servers at random and send to the less busy of the two. Comparing just two gets most of the benefit of checking them all, and because each balancer picks a different random pair, several balancers don't all pile onto the same "least busy" server at once.
- Hashing: send all requests with the same key (a user, a cart) to the same server, useful when servers cache per key (see Consistent hashing).
Slowest 1% of requests (p99), modeled: six servers at 80% busy, each working on one request at a time, request cost varying around a 10 ms median (log-normal, σ = 1). Same servers, same traffic: only the rule for picking a server changes.
Round robin is not wrong. When requests cost about the same and servers are identical, it is simple and even, which is why it is the default in most balancers. Reach for a load-aware rule when request costs vary widely.
4 · Health checks
A balancer only helps if it notices a server has died
If server 4 crashes, a balancer that keeps sending it a sixth of the traffic fails a sixth of your users. It finds out in two ways:
- Active checks: every few seconds to half a minute it asks each server for a small page, often
/health. After a couple of failures in a row the server is taken out of rotation; after several successes it goes back in. - Passive checks: it watches real traffic. A server whose requests keep failing or timing out is set aside for a while, for longer each time it happens again.
Between the crash and the second missed check, requests still go to server 4. With checks every 30 seconds that window is up to a minute; passive checks close it much sooner.
Three traps catch most teams:
Trap | What happens | Fix |
|---|---|---|
The check tests too much | It also checks the shared database; one bad database minute fails every server at once | Check what is local, plus only dependencies the server cannot work without |
The server fails fast | A broken server answers every request with an instant error, looks idle, and attracts more traffic | Count recent errors as load, or eject servers on errors |
Everyone is unhealthy | Too few servers pass the checks | Most balancers fail open: they send traffic to all of them, betting a struggling server beats none |
5 · Servers come and go
Add and remove servers without anyone noticing
Servers leave for upgrades and arrive when you scale out. Both moments can hurt users if the balancer handles them carelessly.
- Drain before removing
Stop sending new requests to the leaving server but let the ones in progress finish. One major cloud balancer waits up to five minutes by default before cutting it off.
- Warm up before full load
A new server has cold caches. A least-busy rule sees it with zero requests and floods it. Slow start ramps its share up over a few minutes.
- Then let the rule take over
Once warm, the new server gets its full share like any other.
Sticky sessions (sending one user to the same server every time, by cookie) make this harder. Load stays uneven after you add servers, because existing users are stuck to the old ones, and a user whose server dies loses whatever was kept there. They are a crutch for keeping state on servers; moving that state into a shared store removes the need.
6 · The balancer itself
The balancer must not be the one box nobody copied
Every request passes through the balancer, so a single balancer is a single point of failure (see Redundancy & failure domains).
Three usual answers, from small to large:
- A pair with a floating address. Two balancers share one virtual IP; the standby takes it over within seconds if the active one stops answering.
- Many nodes behind DNS. Managed cloud balancers run a node in every zone, and DNS hands out all of their addresses.
- Two tiers. At very large scale, a fast layer 4 tier spreads connections across a fleet of layer 7 balancers, which then read each request. The layer 4 tier keeps no shared state: every node computes the same choice from a consistent hash, so any of them can take any packet.
Each tier is many machines, and each does the job it is cheapest at: layer 4 moves connections, layer 7 reads requests.
Inside a company's own network, services often skip the middle box altogether: the calling service learns the list of servers and balances its own requests (client-side load balancing). It saves a network hop and a box to run, at the price of a smarter client in every language you use.
Check yourself
5 questions
Takeaways
Remember this
- A load balancer gives many servers one stable address: it spreads work, routes around failures, and hides servers coming and going.
- Layer 4 balances connections and is cheap; layer 7 reads each request, so it can route by path and spread HTTP/2 and gRPC requests across servers.
- Round robin is even only when requests cost the same; when they vary, least outstanding requests or power of two choices keeps the slowest requests far faster.
- Health checks should test the server, not the whole system; watch for fast-failing servers that attract traffic, and expect balancers to fail open when nothing is healthy.
- Drain servers before removing them, warm them up before full load, avoid sticky sessions, and never run a single balancer.