Dev Skills
← Load balancing·Tier 1 — the basics

Load balancing — in depth

The summary said "spread requests across servers and skip the broken ones". That is true and it hides every decision that matters. This page covers the two layers you can balance at, the main routing algorithms and when each is right, how health checks work and how they can take down whole fleets, sticky sessions and why to avoid them, and the failure modes that are easy to miss.

01

Layer 4 vs Layer 7: what the balancer is allowed to read

The first decision is how much of the request the balancer looks at, and it changes what it can do for you.

Layer 4 works at the TCP level. It sees IP addresses and ports, and does not open the request. It picks a server and copies bytes between the two connections. Because it never parses anything, it is very fast and works for any protocol: HTTP, database connections, game traffic.

Layer 7 reads the HTTP request itself: the path, the headers, the cookies. That means it can send /api to one pool and /images to another, terminate HTTPS, add headers, and retry a failed request on a different server because it still has the request in hand. It costs more CPU per request, because it has to parse and re-serialise, and it only works for protocols it understands.

In short
  • Layer 4 is faster and protocol-agnostic; it cannot make decisions based on what the request says.
  • Layer 7 can route by path, host or header, terminate TLS, rewrite, compress and retry, because it understands the request.
  • Only Layer 7 can safely retry, since Layer 4 has already streamed bytes to a server it cannot take back.
  • Layer 7 sees the client IP and must forward it in X-Forwarded-For, or every backend thinks all traffic comes from the balancer.
  • Default to Layer 7 for web traffic. Use Layer 4 for raw throughput, non-HTTP protocols, or when you must not decrypt.
Layer 4Layer 7
SeesIP and port onlyPath, headers, cookies, body
SpeedVery fast, minimal CPUSlower: parses every request
ProtocolsAnything over TCP/UDPOnly what it understands (HTTP, gRPC)
Can route by pathNoYes
Can terminate TLSUsually passes it through (some, like AWS NLB, can)Yes
Can retry elsewhereNoYes
Typical useDatabases, game servers, extreme scaleWeb and API traffic (the default)
The practical differences.
Say this
"Layer 7, because I want path-based routing and TLS termination in one place, and because it can retry a failed request on another server. If this were database traffic I would use Layer 4 instead. It does not need to read anything, and I do not want the overhead."
02

Round robin: take turns

The simplest possible rule: keep a list of servers and hand each new request to the next one in the list, wrapping around at the end. Server 1, 2, 3, 1, 2, 3.

It works well when two things are true: every server is the same size, and every request costs about the same. Then taking turns spreads load evenly and costs nothing to compute.

It falls apart when requests differ. Imagine one request in twenty is a report that takes ten seconds while the rest take 20 ms. Round robin cheerfully sends the next request to a server already grinding on a report, because it is that server's turn. It has no idea anyone is busy. It counts requests, not work.

In short
  • Hands out requests in strict rotation, ignoring how busy anyone is.
  • Works well when servers are identical and requests cost the same.
  • Bad when request cost varies: it will pile work onto a server that is already struggling.
  • Costs almost nothing to compute and needs no state about the backends.
RequestsRound robin1,2,3,1,2,3…Server 1Server 2Server 3
strict rotation — each server takes the next request in turn, regardless of how busy it is
Strict rotation. The balancer never checks whether a server is busy. It just advances the pointer.
Common trap
Round robin looks fair and is not, whenever request cost varies. The distribution of *requests* is even; the distribution of *work* can be wildly uneven, and work is what saturates a server.
03

Weighted round robin: for servers that are not the same size

Same rotation, but each server gets a weight and receives requests in proportion. A machine with weight 3 gets three requests for every one that a weight-1 machine gets.

This exists because real fleets are rarely uniform. You add newer, bigger instances over time, or you run a mix of instance types to control cost. Without weights, your biggest machine sits at 20% while your smallest is on fire.

It is also the mechanism behind gradual rollouts: give the new version weight 1 against the old version's weight 99, watch the error rate, then shift the weights. That is a canary deploy, implemented entirely in the load balancer.

In short
  • Traffic split in proportion to each server's weight.
  • Use when the fleet is heterogeneous on purpose, with mixed instance sizes.
  • The mechanism behind canary releases and blue-green traffic shifting.
  • Weights are static, so it still does not react to a server being busy.
RequestsWeighted3 : 1 : 1Big serverweight 3Smallweight 1Smallweight 1
a bigger machine gets proportionally more traffic — use when your fleet is not uniform
A 3:1:1 split. The same idea drives canary deploys: send 1% to the new version, watch, then shift the weight.
04

Least connections: the usual default

Instead of taking turns, send each request to whichever server currently has the fewest open connections. It is a live measure of how busy each server is, rather than an assumption that they are all equal.

This handles the case round robin cannot. When a server picks up a slow request, its connection count stays high, so the balancer naturally routes around it until it catches up. Nobody had to configure anything; the metric corrects itself.

For most web traffic, where most requests are fast and a few are much slower, this is the sensible default. Least response time is a refinement that also weighs how quickly each server has been answering, which catches a server that is degraded but still accepting connections.

In short
  • Routes to whichever backend has fewest connections open right now.
  • Automatically avoids a server stuck on slow work, with no configuration.
  • The right default when request cost varies, which is most systems.
  • Requires the balancer to track per-server state, so it is slightly more work than round robin.
  • Least response time additionally accounts for how fast each server is replying.
RequestsLeast connspicks the idle oneServer 18 openServer 21 openServer 37 open
every request goes to the server with fewest open connections — right when requests take wildly different times
Every request goes to the least busy server. When one gets stuck on slow work, traffic flows around it automatically.
Say this
"Least connections rather than round robin, because request cost varies a lot here: some of these calls are 20 ms and some are multi-second exports. Round robin would keep feeding requests to a server already stuck on an export."
05

IP hash and consistent hashing: sending the same user to the same place

Hash something about the request (the client IP, a user id, a cache key) and use the hash to pick the server. The same input always produces the same server.

The reason to want this is cache locality. If each server keeps an in-memory cache, sending the same user to the same server means their data is probably already there. Without it, every server ends up caching everything, which wastes memory and lowers hit rates.

Plain modulo hashing has a serious flaw: hash(key) % N changes for almost every key when N changes. Add or remove one server and nearly every user moves, so every in-memory cache is suddenly wrong at the moment you were trying to add capacity.

Consistent hashing fixes this. Servers are placed around a circle, a key belongs to the first server clockwise from it, and adding a server only reassigns the slice behind it: about 1/N of keys, not all of them.

In short
  • Same client, same server, which is what makes per-server caches work.
  • Plain % N moves almost every key when the server count changes. Avoid it for anything cached.
  • Consistent hashing moves only ~1/N of keys when the fleet changes size.
  • IP-based hashing breaks when many users share one IP: an office, a university or a mobile carrier's NAT all land on one server.
  • Any hashing scheme distributes *keys* evenly, never *traffic*. One very hot key still lands on exactly one server.
Same usersame IPhash(IP)Server 1Server 2always this oneServer 3
the same client always lands on the same server — good for cache locality, bad when that server dies
Deterministic routing: the same client lands on the same server every time.
KeysHash ringclockwiseNode ANode BNEWNode C
most keys keep the home they already hadonly the slice behind the new node moves — about 1/N of keys, not all of them
Adding a node with consistent hashing. Only the keys in the arc behind it move; everyone else keeps their home.
Common trap
Hashing balances keys, not load. If one key is 90% of your traffic, any hashing scheme still sends 90% of traffic to one server. That is a hot-key problem and needs replication of that key, not a better hash.
06

Health checks, and how they take down entire fleets

A balancer is only useful if it stops sending traffic to broken servers, and health checks are how it knows. There are two kinds.

Passive checks watch real traffic: if a server starts returning errors or timing out, mark it down. Free, since it uses requests you were sending anyway, but it only notices after real users have been affected.

Active checks call a dedicated endpoint every couple of seconds. They catch a sick server before users do, at the cost of constant background traffic.

Now the failure that turns a small problem into a total outage. Suppose your health endpoint queries the database to prove the server "really works". The database gets slow. Every health check on every server times out at once. The balancer marks the entire fleet unhealthy and removes all of it. Now nothing is serving, including all the requests that never needed the database.

The fix is to separate two questions. Liveness: is this process alive and able to answer? Cheap, no dependencies, and the balancer uses this one. Readiness: is this instance ready for traffic, including its dependencies? Useful at startup and for orchestration, but dangerous as the sole routing signal. And a good balancer can be told not to remove every instance. If everything looks unhealthy, it keeps serving anyway (some call this fail-open), because degraded beats dead.

In short
  • Passive checks are free but only notice after users are hurt. Active checks catch it earlier and cost background traffic.
  • A health check that touches shared dependencies can mark every server unhealthy at once.
  • Liveness = is the process alive (cheap, no dependencies). Readiness = are dependencies fine. Route on liveness.
  • Configure a minimum healthy pool, so the balancer never removes everything.
  • Connection draining matters: take a server out, let in-flight requests finish, then stop it. Skipping this drops live traffic on every deploy.
Balancerprobes every 2sServer 1healthyServer 2FAILINGServer 3healthy
healthy servers keep receiving trafficthe failing one is probed, does not answer, and is pulled from the pool
The balancer probes each backend. The one that stops answering is pulled from the pool and stops receiving traffic.
Common trap
The health-check cascade is a documented outage pattern: a slow database makes every health check time out, the balancer removes the whole fleet, and a partial degradation becomes a complete outage. Keep the liveness check dependency-free.
07

Sticky sessions: what they solve and why to avoid them

A sticky session pins a user to one server, usually with a cookie the balancer sets. Every request from that user goes back to the same place.

People reach for it when servers keep session state in memory: logged-in user data, a shopping cart, a partly completed form. If request two lands on a different server, that state is not there, and the user gets logged out.

It works, and it costs you more than it looks. Load becomes uneven, because users are not evenly distributed and long-lived sessions accumulate on whichever servers have been up longest. Scaling out helps less, because existing users stay pinned to existing servers and only new users reach the new ones. And when a server restarts, which happens on every deploy, everyone pinned to it loses their state at once.

The better answer is nearly always to make servers stateless: put session data in a shared store like Redis, or in a signed token the client carries. Then any server can handle any request, restarts cost nothing, and the balancer is free to route on actual load.

In short
  • Sticky sessions exist to work around in-memory state on the server.
  • They cause uneven load, weaken horizontal scaling, and lose state on every restart.
  • Prefer stateless servers with session state in a shared store or a signed token.
  • If you must use them, keep the session data recoverable so a lost server is an inconvenience, not a logout.
What it costs you
Stickiness trades away the main benefit of horizontal scaling, that any server can serve any request, to avoid moving session state out of process. That is usually the wrong trade.
08

The balancer itself is a single point of failure

This is a frequent omission in interview diagrams. Someone draws one load balancer in front of five servers and calls the design highly available. It is not: those five servers are now protected by one box, and if it dies, all of them are unreachable.

Real setups run at least two, and there are three ways to make that work. Active-passive with a floating IP: a standby takes over the address when the primary fails. DNS round robin across several balancers: crude, and DNS caching makes failover slow. Anycast: several locations announce the same IP address and the network routes to the nearest one still announcing it. Failover is as fast as routing converges, and large providers use it for their edge.

In practice, if you are on a cloud provider, the managed load balancer is already a redundant distributed system, and the right answer in an interview is to say that explicitly rather than to draw a second box.

In short
  • One load balancer in front of N servers means one failure takes out all N.
  • Run at least two, or use a managed balancer that is already redundant.
  • DNS failover between balancers is slow because clients cache DNS. Anycast is the fast option.
  • Capacity planning must survive losing one: two balancers at 60% utilisation each cannot absorb the other's traffic.
TrafficActiveservingStandbyidle, waiting
all traffic goes to the active nodethe standby only copies state — it is never proven by real traffic until the day it has to take over
Active-passive: the standby is idle and unproven until the day it must take over.
TrafficNode AservingNode Balso serving
both nodes serve constantly, so both are continuously proven to work — and losing one halves capacity, not availability
Active-active: both take traffic constantly, so both are continuously proven. Losing one costs capacity, not availability.
09

What to say in the interview

The points that score.

In short
  • Name the layer and why: "Layer 7, because I want path routing and TLS termination here."
  • Name the algorithm and justify it from request shape: "Least connections, because these requests vary from 20 ms to several seconds."
  • Say the health check is cheap and dependency-free, and say why: the cascade failure.
  • Say your servers are stateless, so no sticky sessions and any server can take any request.
  • Say the balancer is redundant, before the interviewer points out that it is not.
  • Mention connection draining when you talk about deploys.
Say this
"Layer 7 balancer, least-connections, active health checks on a cheap endpoint that does not touch the database. If the check hit the database, one slow query would mark the whole fleet unhealthy. Servers hold no session state, so any server takes any request. At least two balancers, or a managed one, because otherwise the thing I just drew to provide availability is my single point of failure."
Worked example

Putting it together: the front door of a link shortener

Redirects have to answer in well under 200 ms, and the read path is about 4,000 requests a second with peaks near 12,000. Creating a link is rare, around 40 a second, and can afford to be slower. How should traffic reach the servers?

  1. 1
    Pick the layer

    Layer 7, because the read path and the write path want different treatment: /{code} should go to the redirect pool and /api/create to the creation pool. A Layer 4 balancer cannot tell those apart, because it never reads the path.

  2. 2
    Pick the algorithm

    Redirects are uniform: every one is a cache lookup and a 302, a millisecond or two. Round robin handles that case well. But creation involves a database write with a possible retry on collision, so those requests vary. Least connections across both pools is the safer single choice, and it costs almost nothing.

  3. 3
    Health checks

    A /healthz endpoint that returns 200 if the process is up. It deliberately does not check Redis or the database. If the cache is down, redirect servers can still serve from the database, and I do not want the balancer removing every server because one dependency is slow.

  4. 4
    Keep servers stateless

    Redirect servers hold only a small in-process cache of hot codes. Nothing user-specific lives in memory, so no sticky sessions are needed and any server can serve any request. Losing one costs a small cache warm-up, nothing more.

  5. 5
    Make the balancer redundant

    Two balancers, or a managed one. Drawing a single box in front of the fleet would make the component I added for availability the thing most likely to cause an outage.

  6. 6
    Handle deploys

    Remove an instance from the pool, drain in-flight requests, update, wait for the health check to pass, put it back. Without draining, every deploy drops the requests that were mid-flight, which is invisible in staging and obvious in production.

Result: Redirects land on any healthy server in a couple of milliseconds, a dead server leaves the pool within a few seconds, and a slow database degrades link creation without touching the redirect path at all. The costs named out loud: an extra hop of roughly half a millisecond, and two balancers to run instead of one.
Back to the summary and the self-check.← Load balancing