Lakshya

Networks, Automation, Connectivity & Data Centres · Chapter 7 of 18

Load balancing, proxies and the edge

Where traffic gets shaped, and where a bad health check turns a small problem into a total outage.

7 min read1 diagramAll 18 chapters

Every service of any size has something in front of it. Understanding the layer at which it operates, what it can therefore see, and what it does when things go wrong is one of the most practically useful areas in infrastructure — and health checks in particular cause more outages than the failures they are meant to protect against.

LAYER 4 — moves packets sees: source IP, dest IP, ports, TCP flags cannot see: URL, headers, cookies, method can do: round robin, least-conn, hashing cost: microseconds, millions of conn/sec AWS NLB · IPVS · F5 fastL4 · MetalLB LAYER 7 — terminates and re-originates sees: everything, because it decrypts TLS can do: route by path, retry, rewrite, rate-limit,     canary by header, buffer slow clients cost: milliseconds, and it becomes a state holder nginx · Envoy · HAProxy · ALB · Traefik THE HEALTH CHECK IS THE PART THAT CAUSES OUTAGES probe /healthz2 fails → out0 conns left?drain 30sprobe again The trap: a health check that returns 200 whenever the process is alive. If /healthz does not touch the database, a pool with a deaddatabase stays “healthy” and takes 100% of traffic to a 500. The opposite trap: a health check that checks everything downstream. One slow dependency then fails every instance at once and youhave removed the entire fleet.
L4 versus L7, and the health-check loop underneath. The two traps at the bottom are opposites and both are common: a check that is too shallow keeps dead backends in the pool, and one that is too deep removes the entire fleet when a single dependency slows down.

Choosing a layer

A layer-4 balancer forwards packets based on addresses and ports. It cannot see a URL, a header or a cookie, because it never decrypts anything. It is extremely fast, it preserves the client IP naturally, and it can handle millions of connections per second on modest hardware.

A layer-7 balancer terminates the connection, reads the request, and makes a new connection to a backend. That gives it everything — path-based routing, header-based canaries, retries, rate limiting, request rewriting, buffering of slow clients — at the cost of latency, CPU and, importantly, becoming a component that holds state and can itself fail.

The rule of thumb worth stating: use L4 when you need throughput and protocol transparency (databases, gRPC streaming, anything non-HTTP), and L7 when you need to make decisions about the content of requests. Most real architectures use both — an L4 device at the edge spreading across a fleet of L7 proxies.

Algorithms, and the one that is usually right

AlgorithmWhen it is correctWhen it is wrong
Round robinUniform requests, uniform backendsRequests with wildly different costs — one slow endpoint drags one backend down while others idle
Least connectionsAlmost always the right defaultBackends with different capacity, unless weighted
Least response timeLatency-sensitive, heterogeneous backendsCan oscillate — a fast backend attracts load, slows, then is avoided
Consistent hashingCache locality, sticky sessions, shardingUneven key distribution creates a hot shard
Random two-choicesVery large fleetsSlightly worse than least-conn at small scale, much better at very large scale

Least connections is the default that survives contact with reality most often, because it self-corrects: a backend that is slow accumulates connections and is automatically given less work. Round robin does not have that property, which is why a single slow backend under round robin degrades a fixed share of all requests indefinitely.

Health checks: the part that causes the outage

A health check has one job — to answer “should this instance receive traffic?” — and there are two ways to get it wrong, in opposite directions.

  • Too shallow. /healthz returns 200 if the process is running. The database connection pool is exhausted, every real request 500s, and the load balancer cheerfully sends 100% of traffic to instances that cannot serve any of it.
  • Too deep. /healthz checks the database, the cache, and three downstream services. One of those gets slow. Every instance fails its check simultaneously. The load balancer removes the entire fleet and now you have a total outage caused by a partial degradation.
  • The resolution is two endpoints with different meanings, which is exactly Kubernetes' liveness/readiness split: a liveness check that only asks ‘is this process wedged and in need of a restart?’, and a readiness check that asks ‘can this instance serve right now?’ and that is allowed to depend on critical dependencies but not on optional ones.
  • And a floor. Many production balancers implement ‘panic mode’ — if more than some percentage of the pool is unhealthy, ignore health status entirely and load-balance across everything. Degraded service beats no service, and this single setting has saved a great many companies from the too-deep failure above.

Draining, and why deploys cause 502s

When you remove an instance, in-flight requests must be allowed to finish. The sequence that works is: mark unready, wait for the balancer to notice and stop sending new work, then wait for open requests to complete, then stop the process. Skip the middle step and you get a burst of 502s on every deploy, which teams often accept as normal for years.

The subtlety that catches people in Kubernetes: the moment a pod is marked terminating, two things happen concurrently — endpoints are updated, and SIGTERM is sent. Those propagate at different speeds. Adding a preStop sleep of a few seconds before the process begins shutting down closes that race, and is the standard fix for deploy-time 502s.

The edge: CDN, anycast, and where to terminate TLS

A CDN gives you three separable things, and it is worth being able to separate them because teams frequently buy one and only use another. Caching of static assets close to users. Edge termination, where TCP and TLS handshakes complete near the user and only a warm, pooled connection crosses the ocean — this helps dynamic, uncacheable content substantially and is the part most teams under-use. And absorption, where volumetric attacks hit a network with terabits of capacity instead of your origin.

Cache keys are where CDN work actually lives. A cache key that includes a cookie or a user-agent will have a hit rate near zero, and the team will conclude the CDN does not help. Normalising the key — strip irrelevant query parameters, vary only on what genuinely changes the response — is usually the difference between a 40% and a 95% hit rate.

A scenario worth rehearsing: intermittent 502s after every deploy

Symptom. A short burst of 502s for 20–30 seconds after each rollout. Nobody has ever fixed it because it is brief.

Bisect. Is the 502 generated by the proxy or the app? Proxy access logs will show upstream connection failures with a status code and an upstream response time of zero, which means it never got a response — so the app never saw the request.

Cause. Old pods received SIGTERM and closed listeners before the load balancer stopped routing to them. Classic race.

Fix. A preStop hook that sleeps 5–10 seconds, graceful shutdown in the application that stops accepting new connections but finishes in-flight ones, and a terminationGracePeriodSeconds longer than your slowest request.

Why it is a good interview answer. It is a real, small, specific problem, the diagnosis is evidence-based, and the fix is a mechanism rather than a retry.

Check yourself

What is the difference between liveness and readiness, and why does it matter?Liveness asks whether to restart; readiness asks whether to route. Conflating them means a slow dependency triggers restart loops, or a broken instance keeps receiving traffic.
Least connections or round robin?Least connections by default — it self-corrects for slow backends. Round robin only when requests are genuinely uniform.
Your L7 proxy hides the client IP from the application. What do you do?X-Forwarded-For or the PROXY protocol for L4. And you must configure the app to trust it only from your proxy's addresses, otherwise anyone can spoof their source IP in your logs and rate limits.
Why would you deliberately ignore health checks?Panic mode. If most of the fleet reports unhealthy, the check is more likely wrong than the fleet — serving degraded beats serving nothing.
← TLS, certificates, and the outage that happens every yearCloud networking, and the bill it generates →