Module F-11·18 min read

L4 vs L7, round-robin vs least-connections vs hashing, health checks that lie, and connection draining during a deploy.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module F-11 — Load Balancing

What this module covers: The component every request passes through and almost nobody designs deliberately. This module covers what a load balancer actually does beyond distribution, the real difference between L4 and L7 and what each can and cannot do, the distribution algorithms and why least-connections beats round-robin for tail latency, and then the two subjects that cause real outages: health checks that turn one dependency blip into a total outage, and connection draining that drops in-flight requests on every deploy. Plus the layers of load balancing that stack on top of each other, from DNS down to a sidecar proxy.


A Load Balancer Does Four Jobs

Distribution is the obvious one and the least interesting. The full list:

  1. Distribute requests across healthy backends.
  2. Detect health and stop sending traffic to instances that can't serve it.
  3. Terminate TLS, so backends speak plaintext internally and the expensive handshake happens once, close to the user (Module F-5).
  4. Drain — remove an instance from rotation without dropping the requests it's already handling.

Jobs 2 and 4 are where the incidents live. Job 1 is a configuration choice; jobs 2 and 4 are design decisions with failure modes.

Analogy: a load balancer is the host at a restaurant. Seating guests evenly is the easy part. The hard parts are noticing that one section's waiter has walked out (health checks) and closing a section without ejecting the people mid-meal (draining). A host who mishandles either of those empties the restaurant.


L4 vs L7: What Each Can See

L4 (transport layer) balancers forward TCP connections. They see IP addresses and ports and nothing else — the payload is opaque, which is the point: it's cheap, extremely fast, and works for any TCP protocol.

L7 (application layer) balancers terminate the connection, parse HTTP, and make decisions per request. They see method, path, headers, and cookies.

L4L7
Unit of distributionConnectionRequest
Can route by path/headerNoYes
Can retry a failed requestNo (it doesn't know what a request is)Yes
Can rewrite, compress, add headersNoYes
Sees TLS contentNo (unless terminating)Yes (terminates)
OverheadVery lowHigher (parsing, buffering, re-encryption)
Works withAny TCP protocolHTTP, gRPC, WebSocket upgrades

The decisive point, and it follows straight from Module F-6: with HTTP/2 or gRPC, one client holds one long-lived connection carrying thousands of requests. An L4 balancer distributing connections therefore pins all of that traffic to one backend. So:

  • HTTP/1.1 traffic: L4 is often fine; connection count roughly tracks request count.
  • HTTP/2, gRPC, or any long-lived-connection protocol: you need L7 request-level balancing, or client-side balancing, or a mesh sidecar (Module A-5). L4 will distribute connections beautifully and load terribly.

There's also a hybrid worth knowing: an L4 balancer can route by TLS SNI without decrypting, which gives you per-hostname routing while keeping end-to-end encryption. Useful when compliance forbids terminating TLS at the edge.


Algorithms, and What They're Actually Optimising

Round-robin — next backend in sequence. Simple, and correct only when every request costs the same and every backend is identical. Neither is usually true.

Weighted round-robin — proportional to declared capacity. The standard answer for a mixed fleet during a migration between instance types.

Least connections — send to the backend with the fewest in-flight requests. This is the better default for most HTTP services, and the reason is Module F-4: request costs vary enormously (the 900-line order next to the 2-line one). Round-robin keeps feeding an instance that's already stuck behind expensive work; least-connections naturally routes around it, because the stuck instance's in-flight count stays high. It's a feedback signal rather than a fixed rotation.

Least response time — least connections weighted by observed latency. Better still in theory; more sensitive to noisy measurements, and it can amplify oscillation if the measurement window is short.

Hash-based (consistent hashing) — route by a hash of client IP, a cookie, or a URL. Two legitimate uses:

  • Session affinity without server-side session storage — with all four of Module F-9's costs.
  • Cache locality: hashing on the request path means the same URL always reaches the same backend, so its in-process cache stays warm. This is genuinely valuable for cache-heavy services, and consistent hashing (Module P-7) is what keeps it from reshuffling every key when the backend count changes.

Power of two random choices — pick two backends at random, send to the less loaded. Deserves to be better known: it gets most of the benefit of least-connections while needing almost no global state, which matters when the balancing decision is made by many independent clients or sidecars that can't see each other's counts.


Health Checks: How to Cause a Total Outage

This is the most valuable section in the module, because the failure is severe, common, and produced by well-intentioned engineering.

Two distinct questions, often conflated:

  • Liveness: is this process alive and should it be restarted if not?
  • Readiness: can this instance serve traffic right now?

The tempting mistake is a "thorough" health check:

typescript

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.