System Design Foundations
Load Balancing
Millions of Uber phones resolve one hostname at rush hour. No single API box can answer all of them, and you do not want every client to know the private IPs of every replica (an identical copy of the service).
A solves both problems. It sits behind one (or DNS name), picks a healthy backend for each connection, removes bad ones, and shifts traffic during deploys. Good design is choosing where to balance, how smart the pick is, and what happens when retries or partially broken servers pile up.
Ride-request journey
Steer the phone toward a nearby, healthy region before it reaches your servers.
One stable address in front of many API replicas, so clients never learn private IPs.
then
Spread raw connections cheaply, then route by URL path when the request needs it.
Only replicas that pass health checks receive new work; bad ones leave rotation.
What actually happens
The rush-hour story above is one app action for the user. Underneath, may first choose a region via . Inside that region, traffic lands on a . An can spread TCP flows cheaply, because it forwards connections without reading the request inside them; an can terminate TLS, read URLs, and send /trips and /payments to different pools.
The balancer only helps if it knows who is healthy. and gates (a signal from the server that says "I can take traffic now") decide who gets new work. During a deploy, empties old nodes while existing sockets finish.
Day-to-day design is not memorizing vendor QPS (queries per second) numbers. It is matching algorithms to request shape, deciding vs dedicated balancing, and preventing a from turning a slow pool into a self-made outage. The rest of this page follows that order: where to balance, how to pick a backend, how to detect sickness, and how to keep retries from multiplying load.
Run the load balancer and inspect its queues
Each backend has one worker. Normal work takes 200 ms; a slow backend takes 800 ms while still passing readiness. Requests arrive at the interval you choose and wait behind earlier work on their assigned server. Every bar and completion count below comes from those placements and completion times. Changing the algorithm, arrival interval, traffic mix, or weights starts a fresh comparison.
Round robin keeps assignment counts even, although equal counts can mean very different waiting times when C becomes slow.
Simulated time: 0.00 s. Up to 200 requests per run. Reset keeps your selected settings and backend states.
Server A · ready · 0 assigned · 0 outstanding
Server B · ready · 0 assigned · 0 outstanding
Server C · ready · 0 assigned · 0 outstanding
Each bar type uses its own scale across servers; compare blue bars with blue, and amber with amber. Outstanding includes the active request plus its queue.
Send a request to see the first placement. Each backend has one worker, so additional requests wait in its own queue.
0 completed · 0 pending · 0 failed · completed-request mean latency: no completed requests yet.
Inspect the latest 12 requests
| Request / client | Target | Queue wait | State |
|---|
The model isolates scheduling: fixed service times, one worker per backend, one balancer, no retries, no request deadlines, and immediate health-state changes. Real proxies have concurrent workers, delayed observations, and queue limits. Completed-request averages temporarily exclude slow work still pending, so inspect the backlog too.
Start with C slow and send 20 requests using round robin. Switch to least outstanding and repeat with the same arrival rate. Then drain a server while requests are pending and advance time: draining preserves that work, whereas a failure marks it failed. Finally choose client-key hashing with the hot client; extra capacity elsewhere cannot help if affinity keeps sending that client to one server.
Follow the scheduling decision from eligibility to completion
Build a candidate set before applying an algorithm
A balancer first decides which pool serves this traffic: perhaps the orders service, the stable version, and an allowed region. Discovery supplies endpoints for that pool. Readiness, health observations, draining state, and locality policy narrow the eligible set. Only then does round robin, least outstanding, or another algorithm choose a target. A clever scheduling algorithm cannot repair an incorrect candidate list, and a healthy instance in the wrong region or version may still be ineligible.
In the lab, a slow server remains eligible because its readiness check still passes. Marking it draining prevents new assignments while preserving existing work; marking it failed and detected unhealthy removes it and fails pending work. Real detection takes time, so traffic can continue reaching a failed server before the balancer learns about it. Define a separate policy for an empty eligible set: the lab returns an error, while some products can deliberately route to unhealthy targets under a configured fail-open policy.
Choose the unit of work you are balancing
At L4, the placement usually applies to a transport flow. One HTTP/2 connection may carry many concurrent requests, so keeping three TCP connection counts equal does not keep application work equal. At L7, a proxy can select an upstream for each request or stream and reuse its own upstream connections. A WebSocket upgrade becomes a continuing conversation with its selected backend; the proxy does not normally reschedule each message independently.
This is why least connections and least outstanding requests are distinct policies. Ten idle sockets can cost less than one connection carrying a hundred expensive RPCs. Even request counts are only a proxy for cost: one report may use more CPU than a hundred cached lookups. Match the measured load signal to the resource that is actually limited, and preserve explicit concurrency and queue caps instead of assuming the scheduler can absorb unlimited demand.
Explain why a slow queue grows despite spare fleet capacity
Use the lab’s default arrival rate of ten requests per second. A normal worker taking 200 ms can finish five per second; a slow worker taking 800 ms can finish only 1.25. Round robin offers each of three workers about 3.33 requests per second, so the slow worker’s queue grows by roughly 2.08 per second even though the other two still have spare capacity. Equal assignment counts therefore conceal unequal response times.
Least outstanding notices that buildup and directs more new requests toward the normal workers. It cannot move requests already queued, reverse an operation that committed, or create backend capacity. Set arrivals to one every 50 ms and the offered rate becomes twenty per second, above the combined eleven-and-a-quarter requests per second of two normal workers and one slow worker. Every policy eventually needs admission control or more capacity under that sustained load.
How to choose a load balancer
With the moving parts named, the first design decision is which kind of balancer sits in front of your service. Three questions settle most cases.
1. Do you control the client? Browsers and public mobile apps need a dedicated LB or DNS (one IP announced from many places). Internal services with rich SDKs or mesh sidecars (a proxy running beside each service instance) can use to skip a hop.
2. Do you need to read HTTP? Path rules, canaries, a WAF (web application firewall that filters malicious requests), and cookie affinity need . Raw TCP, TLS passthrough (encrypted bytes forwarded unopened), games, and huge WebSocket farms often prefer .
3. Are connections short or long-lived? Short, similar HTTP calls work with round robin (rotate through backends in turn). SSE and WebSocket sockets stay open for a long time, so placing a new one should look at current load with ; the WebSockets section below explains why.
Simple rule of thumb
Default public HTTP: regional L7 (or L4 in front of L7) behind DNS/GSLB, with readiness probes and draining on deploy. Prefer externalized sessions over . Bound retries so overload does not multiply itself.
In simple terms: one address for clients, many healthy backends behind it, and a clear story for failure and deploys.
Vertical vs horizontal scaling
Before going deeper into balancer types, it helps to see why a balancer appears at all: it is the price of scaling out instead of up.
Horizontal scaling surfaces three problems early. You need a routing tier (LB, DNS, mesh, or smart client) so users hit one address while work spreads across nodes. Sessions and caches stop being free in one server's RAM, so you externalize state or accept the stickiness trade-offs covered later. Deploys and health require , probes, and often multi-AZ redundancy (copies in more than one availability zone, a cloud failure domain) so the LB tier itself is not a single point of failure.
High availability pairs those horizontal copies with failure detection: remove bad nodes, and redundant LB appliances or managed multi-AZ endpoints keep routing alive when a zone fails. The LB high availability section near the end shows the common shapes.
Load balancing at every tier
Once you scale out, a single request rarely meets just one balancer. It crosses several hops, and each hop can balance in a different way.
The table maps the same idea onto the hops a typical request crosses inside one region, from the edge down to the database.
| Hop | Typical balancer |
|---|---|
| Edge → CDN | GeoDNS, anycast |
| Edge → frontend | L4 NLB (high conn/sec) |
| Frontend → API | L7 application proxy |
| API → service | Mesh proxy (least-request, outlier detection) |
| Service → DB | Database proxy or connection pooler |
| Service → cache | Consistent hashing across cache shards |
Client-side vs dedicated load balancing
The last two tiers above (mesh sidecars and app-level clients) are where the caller picks a backend itself. That is the client-side option from the first choosing question; here is how it compares with a dedicated balancer.
Dedicated LB (browsers and public APIs)
- Clients hit one or hostname; the LB owns TLS, health, and routing.
- Works when you do not control the client, which is the case for mobile and web.
- The vendor runs the control plane (its management layer); you configure listeners and target groups.
- Adds a hop and cost, but drains take effect immediately without waiting on client caches.
(service-to-service)
- Fewer hops and lower cost at huge internal QPS when every caller has a sidecar or SDK.
- Requires tolerant clients with retries, backoff, and hedging (a duplicate request to another backend).
- Registry updates are eventually consistent, so plan for stale endpoints: old IPs returned briefly after scale-in.
- Fits when membership changes slowly and a briefly stale list is acceptable for a short window.
Tolerating update delays (CSLB, short for client-side load balancing): combine shorter poll intervals with conservative connection pools, exponential backoff on connection errors, and outlier detection so a briefly stale list does not stampede one healthy node. Pair with on the server so new pods are not registered until they pass checks.
Decision tree: when is client-side balancing reasonable?
The same trade-offs in flowchart form. Each "No" sends you back to a dedicated balancer; only a full run of "Yes" answers earns the client-side path.
Need to pick an origin?
Do you own and ship the client code?
No
Use dedicated LB or DNS anycast
Ship via managed LB or mesh ingress
Yes
Check latency budget for discovery
Latency budget allows periodic discovery?
No
Use dedicated LB or DNS anycast
Ship via managed LB or mesh ingress
Yes
Check origin-list stability
Origin list mostly static and stale lists OK?
No
Use dedicated LB or DNS anycast
Ship via managed LB or mesh ingress
Yes
Check L7 edge needs
Need rich L7 rules at edge without a proxy?
Yes
Use dedicated LB or DNS anycast
Ship via managed LB or mesh ingress
No
Client-side LB with SDK and service registry
SDK retries, backoff, and version skew handling
Layer 4 vs layer 7 load balancers
Whichever side does the picking, the balancer works at one of two layers of the network stack, and that choice (question two above) decides what it can see.
For orientation, L3 (the IP layer below both) is where ECMP (equal-cost multipath, spreading packets across equivalent routes) and live; they give packet-path diversity with almost no application context. Typical L4 examples are managed L4 services, TCP-mode proxies, bare-metal L4 services, and many game and VPN front doors. Typical L7 examples are managed L7 services, HTTP ingress proxies, reverse proxies with virtual hosts, and edge HTTP routing.
An L4 TCP proxy uses two connections; packet forwarding is another design
The diagram shows a TCP proxy: it accepts the client connection and opens a separate upstream connection, copying opaque payloads between them. TLS can still terminate at the backend. A packet-based L4 design instead uses forwarding, address translation, or tunneling; direct server return can keep the response path away from the balancer. Do not infer two TCP handshakes from the label L4 alone.
Read the diagram definition
sequenceDiagram
autonumber
participant C as Client
participant LB as L4 TCP proxy
participant O as Origin
rect rgb(30, 41, 59)
Note over C, LB: TCP session A client to LB
C->>LB: SYN
LB-->>C: SYN-ACK
C->>LB: ACK
end
rect rgb(30, 58, 50)
Note over LB, O: TCP session B LB to origin
LB->>O: SYN
O-->>LB: SYN-ACK
LB->>O: ACK
end
Note over C, O: LB splices bytes and does not parse HTTP
C->>LB: TCP payload TLS or HTTP bytes
LB->>O: Forward same payload
O-->>LB: Response bytes
LB-->>C: Response bytes
rect rgb(55, 48, 40)
Note over C, O: Teardown FIN and ACK on both legs
C->>LB: FIN
LB-->>C: ACK
LB->>O: FIN
O-->>LB: ACK
endL7 flow: terminate client HTTP, proxy to backends
The client speaks to the LB as the HTTP server. The LB chooses a backend using path, host, or weights, then acts as an HTTP client on the second hop.
Read the diagram definition
sequenceDiagram
autonumber
participant C as Client
participant LB as L7 ALB style LB
participant O as Origin
rect rgb(30, 41, 59)
Note over C, LB: Client terminates HTTP and TLS at LB
C->>LB: SYN
LB-->>C: SYN-ACK
C->>LB: ACK
C->>LB: HTTP GET path and headers visible to LB
LB-->>C: HTTP response to client may differ from origin
end
rect rgb(30, 58, 50)
Note over LB, O: Separate TCP to chosen origin
LB->>O: SYN
O-->>LB: SYN-ACK
LB->>O: ACK
LB->>O: New HTTP request possibly rewritten
O-->>LB: HTTP response from origin
end
Note over LB: Routing uses URL Host cookies and weightsThe two diagrams show what each layer can see; that visibility is exactly what decides which one to reach for.
When shines
- Non-HTTP protocols, TLS passthrough, or preserving the client source IP without extra headers.
- Very high connection churn where you want minimal CPU spent at the edge.
- You handle HTTP routing elsewhere, for example in an in-cluster ingress proxy.
- Persistent WebSocket or SSE farms: pick a backend once at connect time and stick the flow.
When shines
- Path-based microservices, host-based multi-tenant routing, and header or cookie affinity.
- Central TLS, HTTP/2 from clients, WAF, request rewriting, and canary weights by route.
- The edge must understand protocol upgrades such as WebSocket or gRPC.
- Ordinary HTTP and long polling: path rules, auth headers, and per-request metrics matter more than splicing.
Three questions come up as soon as both layers are on the table.
| Question | Practical answer |
|---|---|
| Which LB is recommended? | No universal winner. Public HTTP APIs: L7 at the edge. Raw TCP services or strict TLS passthrough: L4. Often both in series (NLB → ALB → pods). |
| HTTP/2 multiplexing on one client TCP | L4 maps the whole connection to one backend: all streams share that node. L7 can terminate HTTP/2 and choose an upstream per request or stream, using its own backend connection pools. Verify the product’s routing and affinity settings; request balancing does not inherently require an additional service mesh. |
| Does L7 work with WebSockets? | Yes, when the proxy supports the HTTP Upgrade handshake and keeps the bidirectional tunnel open. You still need stickiness or shared backend state if socket-specific data matters. |
For a concrete L7 example, Envoy models HTTP requests as streams independently of the downstream wire protocol and manages routing and upstream proxying at that level. See its HTTP connection management documentation.
Numbers to know: million-user load math
Layers and algorithms only matter once the pool is sized. Every capacity figure used on this page lives in this one table; later sections refer back here instead of repeating them.
| Check | Calculation | Design implication |
|---|---|---|
| Normal pool | 1.2K peak QPS ÷ 13 replicas | ~92 QPS per replica |
| Safe test limit | p99 starts rising above 120 QPS/replica | Scale before this, around 90-100 QPS/replica |
| One zone lost | 13 replicas placed 5 / 4 / 4; lose the zone with 5 | 8 survive: 1,200 ÷ 8 = 150 QPS each, above the assumed safe limit |
| Retry storm | 1.2K QPS × (1 + 2 retries) | Up to 3.6K attempted QPS |
| Health check load | targets × balancer nodes ÷ interval | Use shallow checks unless dependency-aware checks are needed |
The table assumes a load test found 120 requests per second to be the upper safe limit for this workload and configuration. Thirteen replicas cover the normal 1,200-request peak, but a 5/4/4 placement loses five when the largest zone fails, pushing each survivor to 150. Fifteen replicas placed 5/5/5 leave ten and merely meet the 120 limit after a zone loss; eighteen placed 6/6/6 leave twelve at 100 each. Choose the fleet from the required failure case and remaining margin, not only the normal average.
The benchmark should include realistic TLS reuse, request sizes, expensive routes, downstream latency, and connection duration. Measure completed goodput and tail latency as offered load rises. A balancer accepting more connections while requests pile up is not evidence of useful extra capacity, and a shared database bottleneck remains even if you add more application servers.
The retry row is the one that surprises people, so here it is as a flow: the same incoming traffic, tripled by two well-meaning retries.
1.2K incoming QPS
Load balancer
API replica pool
Timeouts?
Client retry 1
+1.2K attempted QPS
Client retry 2
+1.2K attempted QPS
3.6K attempted QPS worst case
Timeout budgets, jitter, circuit breakers, load shedding
Retry storms and gray failures
Removing a slow backend shifts its traffic onto the survivors. If they are already near saturation, aggressive ejection can produce a cascade in which each newly overloaded instance also fails health checks. Combine ejection limits with admission control and spare capacity. Retry only within a total request budget, and measure backend attempts separately from incoming user requests so retry amplification remains visible.
The table shows the arithmetic; this is how to limit it. Three habits, then a failure test that checks their interaction.
Bound retries. Use one or two retries only where operations are idempotent (safe to repeat without side effects), add jitter (a small random delay), and set a total deadline so incoming QPS cannot multiply the way the retry row above shows.
Detect gray failures. A gray failure is a node that passes TCP connect while its p99 latency (the time at or below which 99% of measured requests finish) and 5xx errors are bad. Combine readiness, passive outlier detection (watching live errors rather than probes), and slow-start so partially broken nodes drain instead of attracting retries.
Load-shed safely. When every backend is saturated, return fast 429 or 503 responses for low-priority work instead of queuing until clients time out and retry. Preserve capacity for health checks and critical routes.
Test recipe
Inject 2-second latency into 20% of replicas, then verify the LB stops sending new work to outliers, clients obey retry budgets, and backend QPS stays under the per-replica safe target from the table above.
Managed load balancer offerings
Cloud vendors package the L4 and L7 shapes above under different product names. The names change; the layer each one works at does not.
| Offering shape | Layer | Typical use |
|---|---|---|
| Application load balancer | L7 HTTP/HTTPS/gRPC/WebSocket | Path/host routing, TLS termination, slow starts, edge security rules. |
| Network load balancer | L4 TCP/UDP/TLS passthrough | Static IPs, preserve client IP, extreme conn/sec, non-HTTP protocols. |
| Gateway load balancer | GENEVE (a tunnelling protocol) to appliances | Inline firewalls/IDS inspection without hairpinning through a proxy VM fleet manually. |
| Classic load balancer | Legacy mix | Older stacks; prefer ALB/NLB for new designs. |
| DNS / edge routing | DNS / edge | Geo routing, health-based failover, caching before traffic hits your regional LB. |
GSLB and DNS-based balancing
The last row of that table, DNS and edge routing, is the first hop from the tier list: deciding which region a user reaches at all.
picks a datacenter per query using latency, geography, weighted splits, or failover rules. advertises the same IP from many POPs (points of presence, the edge sites) so BGP (the routing protocol between networks) sends users to the closest surviving edge automatically.
GSLB decision sketch (per query)
DNS limitations beginners hit
Cached answers delay failover. Mobile apps and JVMs sometimes cache DNS aggressively unless tuned. Pair low TTLs with anycast or health-checked proxies when you need faster reroutes than DNS alone.
Algorithms (how picks are made)
The scheduling unit matters as much as the algorithm. Round robin per request can distribute small independent HTTP calls evenly, while round robin per connection may put thousands of HTTP/2 streams on one backend. Least connections helps when connection lifetimes differ, but one quiet socket and one busy video stream are not equal work. Choose a load signal that tracks the scarce resource and validate it under the actual request mix.
Every tier so far answers "which balancer". Inside any of them, an algorithm answers "which backend gets the next unit of work".
Round robin and weighted round robin cycle A → B → C → A. Weights let a 2× machine receive twice as many new assignments when its measured capacity supports that choice. Example: three identical API pods behind an L7 balancer with equal weights each get about one third of new HTTP requests if sessions are short. Best when work units are similar in duration and backends are homogeneous.
and least outstanding requests use different counters: open connections for the former, active requests or streams for the latter. If one backend accumulates unfinished reports, an outstanding-request policy sends less new work there. Neither policy measures future request cost, and an idle keep-alive socket should not be mistaken for an expensive request. Choose the counter that represents the workload.
Hashing (by IP, cookie, or header) and consistent hashing send the same key to the same node. Example: cache shards. user_id % N is naive; consistent hashing (Ketama-style rings) moves fewer keys when N changes. Best for caches, stateful games, or when you accept hot shards to preserve locality.
Random and power-of-two choices (Po2C) pick two random backends and send to the lighter one. Example: a massive worker pool where maintaining global least-connections state is expensive; Po2C reduces tail latency without a central coordinator. Best at huge fan-out with cheap local decisions.
Latency-aware and slow-start. Some proxies ramp traffic to cold nodes slowly (slow start) so JIT compilation (code being optimised while it runs) or empty caches do not overload a newcomer. Combine with outlier detection to eject backends whose live error rate spikes even while probes are green.
Work through the weighted decision
With weights A=2, B=1, C=1, smooth weighted round robin starts running scores at zero. Add the weights to get 2/1/1, choose A, then subtract the total weight of four from A, leaving −2/1/1. Repeat before the next request: the scores become 0/2/2, so a deterministic tie rule can choose B. Over complete cycles A receives half the assignments and B and C a quarter each. Smoothing spreads a heavy target’s turns through the cycle rather than sending one large burst.
That arithmetic encodes a configured share, not observed speed. If all three workers have identical capacity and you double A’s weight, you can overload A earlier. If A is genuinely twice as capable for this workload, the same weights can be appropriate. Likewise, a canary’s weight limits its exposure but says nothing about its correctness. The lab intentionally lets you see both cases without secretly changing a server’s speed when its weight changes.
Decide how much load information a picker can afford
Scanning every backend for the smallest outstanding count costs work proportional to the pool size. Sampling two candidates needs only two observations and usually avoids repeatedly choosing an already busy server. With several independent proxies, each often sees only its own active requests or a delayed report, so “least loaded” is not an instantaneous global fact. Identical reactions to stale measurements can send many callers toward the same apparently idle server; randomized choice helps reduce that crowding.
Product names need careful reading. Envoy’s equal-weight least-request policy uses a configurable random sample, with two choices by default, while the lab’s least-outstanding option scans all eligible servers to make the distinction visible. Its weighted behavior is also a separate policy. See Envoy’s load balancing policy definitions and NGINX’s explanation of weights and connection counts.
The same families in table form, with the interview fit for each.
| Algorithm | Picks by | Interview fit |
|---|---|---|
| Round robin | Rotate equally | Short, similar HTTP requests on homogeneous pods. |
| Random | Uniform pick (or power-of-two) | Huge pools; cheap local decisions without global least-conn state. |
| Least connections | Fewest open sockets / in-flight | SSE and WebSockets especially: long-lived streams skew load; new upgrades should prefer lighter nodes. |
| Least response time | Recent latency / outstanding work | Variable request cost; avoid feeding a slow replica. |
| IP hash (affinity) | Hash client IP → backend | Crude stickiness when you lack cookies; watch NAT/CGNAT collapsing many users onto one IP. |
How to choose an LB algorithm
With the families in view, the pick usually comes down to a short mapping and four questions about your traffic.
Quick chooser
- Round robin: homogeneous stateless APIs where every request costs about the same.
- Weighted: mixed instance sizes, canaries, or gradual migrations that need uneven splits.
- Least outstanding: variable request time such as uploads, reports, and long polls.
- Consistent hash: cache locality, rooms, games, or sticky state you cannot externalize yet.
Questions worth asking
- Do requests have similar cost, or heavy-tail durations that make round robin unfair?
- Can any backend serve any request, or does some state require affinity?
- What happens to long-lived connections during deploys, and who drains them?
- Which health signal removes a target: probe failure, error rate, or latency outlier?
WebSockets, SSE, and hour-long connections
New capacity only helps new placements unless the system can reconnect existing clients. If one gateway holds most active sockets, adding five empty gateways does not migrate those sockets automatically. During deployment, stop assigning new connections to the old gateway, let short work finish, then ask long-lived clients to reconnect with randomized delay and a resume token. The durable event history must survive that gateway’s removal.
Two of the questions above (long-lived connections and affinity) dominate one workload in particular, so it gets its own section.
- or bounded loads when opening new upgrades, so busy nodes receive fewer long-lived sockets.
- Layer choice follows the L4 vs L7 section above: L7 only when you need path-based Upgrade routing.
- Consistent hashing on user or room id when you need memory-local fan-out, staying aware of hot keys.
- Drain plus connection age: stop sending new sockets to an old version while existing sockets age out.
- Use a publish-only bus or stream for cross-node events when clients on different machines must chat.
- Align idle timeouts across LB, proxy, and app keepalives so intermediaries do not silently half-close you.
- SSE heartbeats matter for the same reason: a quiet stream looks idle to every timeout in the path.
Health checks (deep dive)
A health check is a delayed observation, not an immediate guarantee. With a five-second interval and three failed checks required, a failed backend can receive traffic for several seconds before removal. Requests already assigned to it need their own timeout and recovery behavior. Passive error measurements can react to failures that a lightweight health endpoint misses, while hysteresis prevents a briefly recovered node from repeatedly entering and leaving service.
Every algorithm above assumes the candidate list is already trustworthy. Health checks build that list, and the details decide how long a sick node keeps receiving traffic.
Can the target still accept connections while unhealthy? Physically yes: the process is still listening, but a well-configured LB stops scheduling new connections to it after enough failed probes. Some modes allow active sockets to linger while the target is out of rotation for new work.
Healthy and unhealthy thresholds: vendors require consecutive successes before marking a target in (avoids flapping on a single lucky probe) and consecutive failures before marking it out (avoids dropping a node on one timeout). Tune intervals against deploy speed and dependency blips. Split endpoints for so the balancer mirrors readiness, not only process existence.
Three kinds of signal feed a . Active probes are synthetic hits on an endpoint; match the protocol your users use (HTTP for L7, TCP connect for L4). Passive outlier ejection tracks live traffic for spikes in 5xx or latency and temporarily drops noisy hosts without waiting for the next probe interval. Synthetic journeys run scripted flows from multiple regions to detect DNS, TLS, or routing regressions before users notice.
TCP vs HTTP checks
TCP health checks
Succeed if a TCP connect (and maybe a SYN/ACK, the handshake reply) works. Cheap and protocol-agnostic: good for L4 and non-HTTP listeners. Blind spot: the process can accept sockets while the app is wedged (full thread pool, broken dependency). Prefer readiness HTTP when the service speaks HTTP.
HTTP health checks
GET a readiness path; expect 2xx. Can assert "I can serve traffic" (optional shallow dependency checks). Heavier than TCP and can amplify probe load: keep the handler cheap. Pair with passive outlier detection for gray failures TCP would miss.
Health check math
Assume probes start every ten seconds, each failed probe consumes a two-second timeout, and removal needs three consecutive failures. A crash just before a scheduled probe is detected after roughly 22 seconds; a crash just after the last good probe can take roughly 32. This 22–32 second range follows the probe schedule and excludes propagation delay and jitter. A product scheduling its interval after probe completion has different timing, so check its exact semantics.
Probe load is a separate calculation: 500 targets checked independently by three balancer nodes every ten seconds produce 500 × 3 / 10 = 150 probe requests per second. A health endpoint that runs a costly database query turns that into continuous extra database load. A cheap readiness check needs complementary user-path observations to find problems it cannot exercise, rather than making every probe an expensive end-to-end test.
Decide what happens when every target is unhealthy. Failing closed returns a clear unavailable response; failing open tries targets despite their health state and can help when a faulty health policy removed working servers, but can also amplify a real outage. This choice, thresholds, and allowed response codes are configuration decisions; Envoy’s active health checks illustrate why there is more to health than whether a port is open.
Recover without ejecting the entire fleet
Suppose one of ten equally loaded servers fails. Removing it increases the average traffic on each survivor by a factor of 10/9, about 11%. If five fail, that factor is two. Ejection is therefore also a capacity event. Set a bound on how much of the pool passive outlier detection may remove, preserve capacity for health and control traffic, and shed optional work before the survivors become unhealthy in turn. Inspect local connection errors separately from application responses where the proxy supports that distinction; a shared network failure should not be mistaken for ten independent bad applications.
Recovery needs hysteresis: a server should pass enough checks before re-entry, and cold caches or JIT warmup may justify gradually increasing its share. A recovered server with zero outstanding requests can otherwise attract a sudden rush. Active probes and passive ejection can interact, so test whether a passing probe unejects a host that still fails real requests. These interactions and ejection limits are described in Envoy’s outlier detection documentation.
Health signal hierarchy
The checks above sit on a ladder: each rung proves more about the real traffic path and carries its own risk.
| Signal | Proves | Risk |
|---|---|---|
| TCP connect | Process is listening | App may be wedged while port is open |
| HTTP readiness | Shallow request succeeds | DB dependency may still be broken |
| Dependency-aware readiness | Real traffic path likely works | Too strict checks can cascade-remove entire pools |
| Passive outlier detection | Gray failures (slow/error tail) | Needs enough traffic for statistical confidence |
Stickiness and draining
Health checks remove nodes that fail. Deploys remove nodes on purpose, and how gracefully that happens depends on two settings.
Why stickiness hurts. Load goes uneven when power users cluster on one node. A crash wipes every in-memory session on that node. Scaling down requires careful drains, because a pinned user cannot move without losing state. Rolling deploys break naive sticky setups for the same reason.
Draining checklist. The order matters: take the node out of the picture first, then wait for work that is already there.
- Remove the instance from rotation so the balancer stops scheduling new connections to it.
- Wait for in-flight HTTP and RPC calls to finish rather than cutting them off.
- Honor long-lived sockets: WebSockets and gRPC GOAWAY (the graceful shutdown frame) need time to close.
- On container platforms, set the termination grace period and pre-stop hooks to match.
Blue-green and canary via the LB
Draining makes it safe to take one node out. Blue-green and canary deploys use the same traffic-shifting control to take a whole version out.
Blue-green
- The current (blue) pool serves all traffic while nothing else changes.
- Deploy the new (green) pool in parallel; it receives no user traffic yet.
- Smoke test it privately before any user traffic is routed there.
- Switch the routing policy for new requests to green; allow old in-flight work and long-lived connections to drain under a defined deadline.
- Keep the old pool warm so rollback is another flip, not a redeploy.
- Route 1-5% of traffic to the new version behind an LB weight.
- Watch errors, latency, and business KPIs (the key business metrics) for that slice.
- Ramp gradually after enough observations, or restore the previous weights for new placements; configuration propagation and in-flight work mean rollback is not literally instantaneous.
LB high availability
Everything so far assumes the balancer itself stays up. It is also a machine, so it needs the same redundancy it gives your backends.
Reference throughput (orders of magnitude)
With the shapes of a highly available tier covered, here is roughly how much each shape can move. Treat these as talk-track comparisons, not benchmarks.
| Balancer | Talk track |
|---|---|
| Managed L7 LB | High HTTP throughput per AZ; terminates TLS and routes paths. |
| Managed L4 LB | Millions of flows/sec class hardware path for TCP/UDP. |
| Software proxies | Software LBs tuned on bare metal stay relevant at huge QPS. |
| Service mesh proxy | Rich observability and policies per service instance. |
| Consistent-hash L4 design | L4 design pattern using commodity servers + consistent hashing tables. |
Failure timeline: gray node plus retry storm
Putting health checks, algorithms, and retry budgets together, here is how a single gray node plays out second by second, followed by the rule the whole design exists to protect.
t=0 Pool healthy: 1.2K QPS across 13 replicas (~92 QPS each) t=10s One replica: TCP probes green, p99 → 3s, 30% 5xx t=15s Round-robin still feeds it; clients timeout and retry ×2 t=20s Attempted QPS → ~3.6K; healthy replicas saturate t=25s Without outlier ejection: cascade to pool-wide timeouts t=30s With passive ejection + retry budget: sick node drains; load sheds Outcome Health checks alone are not enough: bound retries and eject gray failures
What must never become false
Only backends receive new work. A instance gets no new sockets while in-flight work finishes. Client and proxy retries stay inside one budget so offered QPS cannot silently multiply past the safe per-replica limit (see Numbers to know above) during gray failures.
Wrap-up
You have seen where to balance, how picks are made, how sickness is detected, and how retries turn a slow node into an outage. Check the ideas with these exercises, then follow the references for the primary sources.
Exercises
- Pick L4 vs L7 for a WebSocket chat farm and a path-routed REST API. Justify the algorithm you would use for placing new sockets.
- A health check runs at a 10s interval and needs 3 failures. How long can a wedged but TCP-accepting node keep getting new HTTP traffic? Fix the probe design.
- A gateway retries twice and the app retries twice on the same idempotent GET. Cap the stack so attempted QPS cannot climb past the safe per-replica limit in Numbers to know.
References
- Envoy Proxy docs: outlier detection, circuit breakers, and load balancing algorithms in one data plane.
- NGINX and HAProxy documentation: upstream health checks, connection draining, and least-conn configuration.
- Google SRE Book: the Load Balancing at the Frontend and Handling Overload chapters.
What to remember
Explain DNS vs L4 vs L7 responsibilities, pick an algorithm with a justification tied to request shape, describe (TCP vs HTTP, thresholds, ) and , and prefer stateless backends with externalized sessions. Be ready to contrast with a dedicated hop using the trade-offs from that section.
What LB do we use? Answer with your real stack in generic terms: "Public traffic lands on an L4 endpoint for static IP and TLS passthrough, then an L7 proxy terminates HTTP/2 and routes to service ingress; east-west calls (service to service) use a mesh with least-request and outlier detection." If unknown, say how you would choose (protocol, TLS ownership, routing granularity, ops cost).
Practice and further reading
Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.
Focus for this lesson: Distribute admitted work using health, capacity, locality, connection behavior, slow start, and outlier signals—not round robin alone.
Explain the decision
Why can retries at a load balancer worsen an outage?
Read the reasoning
They multiply load on a shrinking healthy pool; retries need a budget, attempt cap, deadline, backoff, and idempotency.
Apply it to a production scenario
New instances fail health checks under immediate traffic after deploy. What changes?
Read one approach
Use readiness gates, slow start, connection pre-warm, bounded concurrency, and outlier detection that distinguishes cold start from persistent failure.
Work through the numbers
A tier receives 60,000 QPS; each instance safely handles 1,200 QPS. Size for one-zone loss from three equal zones.
Read the worked calculation
Normal minimum is 50 instances; surviving two zones must handle all load, so provision roughly 25 per zone, 75 total, plus desired headroom.
Reason through a failure
What must health checking avoid?
Read the recovery reasoning
It must not mark instances healthy when their critical dependency path is unusable, nor eject the entire pool due to a shared downstream failure.
Sources and further study
Continue with Caching.