Tier 0 · Foundations

Load Balancing

L4 vs L7, the algorithms that matter, and how health checks cause outages

⏱ 16 min foundationsavailabilitytraffic 📕 Ch 1 — Scale From Zero To Millions

L4 vs L7

The layer decides what the balancer can see, and therefore what it can do.

L4 (transport) L7 (application)
Sees IP, port, TCP/UDP URL, headers, cookies, method, body
Terminates TLS No (usually) Yes
Can route by path/host No Yes
Can retry a failed request No — it's a byte stream Yes
Throughput Very high, low CPU Lower, more CPU
Examples AWS NLB, IPVS, Maglev AWS ALB, Envoy, NGINX, HAProxy

L4 forwards packets, so it's fast and protocol-agnostic — good for raw TCP, databases, and gigantic throughput. L7 understands HTTP, so it can route /api/* to one pool and /static/* to another, rewrite headers, retry idempotent requests, and enforce rate limits.

Most production stacks use both: L4 at the edge for volume and DDoS absorption, L7 behind it for routing intelligence.

The algorithms

Round robin — next server in rotation. Simple, and fine when requests are uniform and servers identical. Neither is usually true.

Weighted round robin — round robin with capacity weights. The standard fix for a fleet with mixed instance sizes.

Least connections — send to whoever has the fewest in-flight requests. Naturally adapts to uneven request costs, because an expensive request keeps a connection occupied. A good default for HTTP.

Least response time — least connections, weighted by observed latency. Better in theory; can oscillate, since everyone piles onto whichever server just looked fast.

Hash-based — hash the client IP or a URL component to pick a server. Gives you stickiness without cookies, and is how cache tiers get key affinity. Use consistent hashing so adding a server doesn't reshuffle everything.

Power of two choices — pick two servers at random, send to whichever has fewer connections. This one deserves more attention than it gets: it needs no global state, and it gets you most of the benefit of least-connections while avoiding the herd behaviour where every balancer simultaneously decides the same server is idle.

Health checks, and how they cause outages

Active — the balancer probes /health on a schedule. Passive — it watches real traffic and ejects a backend after N consecutive failures. Use both: active catches a server that's idle-but-broken, passive catches failure modes your probe doesn't cover.

The important design decision is shallow vs deep:

Deep checks sound more responsible, and they are how you turn a partial outage into a total one:

Related trap: health-check-driven cascading failure. One server is ejected, its traffic redistributes to the rest, they get slower, they start failing checks, they get ejected. The cluster eats itself one node at a time. Rate-limit ejections and never let the pool shrink below a floor.

Sessions, and why you should not need stickiness

Sticky sessions (a cookie pinning a client to one server) are easy and they cost you:

The alternative is to make servers stateless: put session state in a shared store (Redis) or in a signed token (JWT) the client carries. Then any server can serve any request, and almost every operational problem above disappears.

Stickiness is still legitimate for long-lived connections — WebSockets pin to a server by nature — and for cache affinity, where you want the same key on the same node.

Graceful shutdown

The most common self-inflicted 502. Deploying means removing servers, and the sequence matters:

  1. Mark the instance unhealthy (or deregister it) so it stops receiving new connections.
  2. Wait for the balancer to notice — it's polling, so this takes a health-check interval or two.
  3. Finish in-flight requests (connection draining), with a timeout.
  4. Then exit.

Skipping step 2 is the classic bug: the process exits the instant it deregisters, while the balancer is still sending it traffic. Those requests become 502s during every deploy.

Where balancers sit

The balancer must not be a single point of failure itself. In practice: an active-passive pair sharing a virtual IP, or an anycast address advertised from several places.

What to take away

Check yourself

  1. Requests to your service vary widely in cost — some take 5 ms, some take 2 s. Which algorithm handles this best?

  2. Your health check verifies database connectivity. The database slows down. What happens?

  3. Why can 'send each request to the least loaded server' behave badly with many independent load balancers?

  4. During every deploy, users see brief 502 errors. What is the most likely cause?