Notes
The problem
Start with the problem. One app server has a ceiling: so many cores, so much memory, so many open connections. It's also a single point of failure, and every deploy restarts it. The fix is many identical stateless servers. But clients need one address to talk to, and something has to spread the work. That something is the load balancer.
- capacity: one box tops out
- (CPU, memory, connections)
- availability: it dies,
- the whole product is down
- deploys: restarting it
- means downtime
- fix: many identical servers
- and something to spread load
What it does
The load balancer does four things. It accepts traffic on one stable virtual IP. It chooses a healthy backend. It forwards the request and the response. And it protects the pool: it stops sending to dead servers, can shed load, and usually terminates TLS. Clients never learn how many servers exist, so you can add and remove them freely.
- accept: one virtual IP or
- hostname for all clients
- choose: pick a healthy backend
- forward: proxy the request
- and the response
- protect: drop dead servers,
- shed load, terminate TLS
L4 vs L7
There are two kinds. A layer four balancer sees only IP addresses and ports. It forwards whole TCP connections, which is extremely fast, millions of packets a second, but it can't read paths or cookies. A layer seven balancer terminates TLS and parses HTTP, so it can route /api to one pool and /images to another, retry, and balance each gRPC call. It costs more CPU per request.
- L4 (TCP/UDP):
- sees IPs and ports only
- forwards connections
- millions of packets/s
- no TLS, no paths, no cookies
- L7 (HTTP/gRPC):
- terminates TLS, reads request
- routes by path, header, host
- retries, gRPC per-request LB
- more CPU per request
Inside one request
Follow one request through an L7 balancer like Envoy or Nginx. It accepts the connection and finishes the TLS handshake. It parses the host, path and headers, matches a route to a backend pool, and picks a healthy server. It reuses a warm keep-alive connection to that server, adds headers like X-Forwarded-For and a request id, waits with a timeout, and streams the answer back.
1 accept TCP, finish TLS 2 parse HTTP: host, path, headers 3 match a route → backend pool 4 pick a healthy backend (algo) 5 reuse a warm upstream connection 6 add X-Forwarded-For, request id 7 wait with a timeout, maybe retry 8 stream response back, log it
Algorithms
Now the algorithms. Round robin hands requests out in turn; weighted round robin gives bigger machines more. That's fine when every request costs about the same. When cost varies, like a mix of quick reads and slow reports, use least connections. At large scale, power of two choices samples two servers at random and picks the less busy one, which avoids everyone stampeding the same least-loaded box.
- round robin: next in turn
- weighted RR: big boxes get more
- least connections: fewest
- in-flight requests
- power of two choices: sample 2,
- pick the less busy one
- hash (IP, user, key): same
- client → same server
Affinity
Sometimes you want the same client on the same server: a WebSocket room, a local cache, an in-memory session you haven't moved out yet. Use a sticky cookie or hash a key like the user id. Plain hash mod N moves almost every key when a server is added, so use consistent hashing, from Concepts episode twenty-two. Prefer stateless servers, though; stickiness makes failover and scaling harder.
- cookie stickiness: LB sets a
- cookie naming the backend
- hash routing: hash(key) picks
- the server (cache locality,
- WebSocket rooms, shards)
- plain mod N reshuffles all keys
- when N changes → use
- consistent hashing (Concepts 22)
Health checks
How does it know a server is dead? Two ways. Active checks probe a health endpoint every few seconds: three failures and the server is out, two passes and it's back. Passive checks watch real traffic: five errors in a row and the server is ejected for thirty seconds. The pool itself usually comes from service discovery, so new servers join without anyone editing a config file.
- active: probe /healthz every
- 5 s; 3 failures → out,
- 2 passes → back in
- passive (outlier detection):
- 5 errors in a row → eject
- for 30 s
- pool from a registry or
- service discovery (Consul,
- Kubernetes endpoints)
Draining
Deploys are where careless setups drop requests. Before stopping a server, drain it: the balancer stops sending new requests but lets in-flight ones finish, say for thirty seconds. When a new server joins, slow start ramps its weight up over a minute so its cold caches don't get flattened. And make the health endpoint check real readiness, not just that the process is running.
- connection draining:
- mark server "draining"
- no new requests
- finish in-flight (e.g. 30 s)
- then stop it
- slow start: ramp a new server
- from 0 to full weight over
- 30-60 s (cold caches, JIT)
- health endpoint ≠ "process up"
When the LB dies
The balancer is now your single point of failure, so it comes in pairs. In active-passive, two machines exchange heartbeats, and if the active one goes quiet the standby claims the virtual IP, usually within a few seconds. In active-active, several balancers all take traffic, spread by DNS or by routers using equal-cost multipath. Managed cloud balancers do this for you, but they still have quotas and warm-up behaviour.
- active-passive pair:
- heartbeat (VRRP/keepalived)
- standby claims the virtual IP
- failover in ~1-3 s
- active-active:
- several LBs behind DNS
- or ECMP / anycast routing
- cloud LBs do this for you
- (but they still have limits)
Scaling the tier
One box can't terminate TLS for a whole large site, so big systems use two tiers. A layer four tier, spread by routers with equal-cost multipath or anycast, hashes each connection to one of many layer seven proxies. The proxies do the expensive work: TLS, routing, retries. Need more capacity? Add proxies. DNS hands out several entry IPs with a short TTL.
- tier 1: L4, ECMP or anycast
- very cheap per packet
- tier 2: fleet of L7 proxies
- TLS, routing, retries
- add L7 nodes horizontally;
- L4 hashes the connection
- (5-tuple) to keep it on one
- DNS: several IPs, short TTL
Failure modes
Name the failure modes before the interviewer does. Cascades: one of three servers dies, the other two take fifty percent more and fall over too, so keep spare capacity. Retry storms: retrying every timeout can double or triple load, so cap retries with a budget, say ten percent. And flapping checks: if half the pool looks unhealthy at once, it's probably the check, so a panic threshold keeps routing to everyone.
- cascade: 1 of 3 dies, others
- take +50% and fall over
- → keep N+1 headroom
- retry storm: LB retries on
- every timeout → 2-3x load
- → retry budget (e.g. 10%)
- flapping health checks
- → hysteresis, panic threshold
- (ignore checks if 50% fail)
Global
Across regions there's one more layer. Geo or latency-based DNS answers with the nearest healthy region's address, but failover is only as fast as clients respect the TTL. Anycast advertises the same IP from every location, and internet routing delivers you to the nearest one, so failover takes seconds. Inside each region, it's the same balancer story again.
- GeoDNS / latency DNS:
- answer with nearest region
- failover = change answer
- (slow: TTLs, caching)
- anycast: same IP everywhere,
- BGP sends you to the nearest
- edge; failover in seconds
- then a regional LB as before
Sizing
Sizing is simple arithmetic with stated assumptions. Say sixty thousand HTTPS requests a second at peak. If one eight-core proxy handles about fifteen thousand a second with TLS, which you'd benchmark, you need four, so run six for failures. If an app server handles four hundred a second, that's a hundred and fifty, plus thirty percent headroom. And by Little's law, sixty thousand a second times two hundred milliseconds is twelve thousand requests in flight.
peak: 60,000 req/s HTTPS one L7 proxy (8 vCPU): ~15,000 req/s with TLS (measure it!) → 4 proxies, run 6 (N+2) app server: ~400 req/s each → 150 servers + 30% = ~200 conns: 60k rps × 0.2 s = 12k in flight (Little's law)
Say it like this
In the interview, draw DNS, a redundant balancer, and a pool of stateless servers. Then justify it in a sentence each: layer seven because you need TLS termination and path routing; least connections or power of two choices because request cost varies; active plus passive health checks; a redundant pair or a managed balancer so it isn't a single point of failure. Mention draining, headroom and retry budgets, and you sound like you've run one.
- draw: DNS → L4/L7 LB pair
- → stateless app pool
- say: L7 for TLS + path routing
- algorithm: least-conn or P2C
- health: active + outlier
- HA: pair with floating IP
- or managed (ALB, NLB, GCLB)
- mention: draining, headroom,
- retry budget, global DNS