SwadhyAI Notes

All System Design notes

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

1

Steer the phone toward a nearby, healthy region before it reaches your servers.

2

One stable address in front of many API replicas, so clients never learn private IPs.

3

then

Spread raw connections cheaply, then route by URL path when the request needs it.

4

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.

Choose the experiment

Round robin keeps assignment counts even, although equal counts can mean very different waiting times when C becomes slow.

Backend state · changes take effect at the current simulated time
Server A
Server B
Server C

Simulated time: 0.00 s. Up to 200 requests per run. Reset keeps your selected settings and backend states.

Assignments and outstanding work tell different stories

Server A · ready · 0 assigned · 0 outstanding

Total assigned
Still outstanding

Server B · ready · 0 assigned · 0 outstanding

Total assigned
Still outstanding

Server C · ready · 0 assigned · 0 outstanding

Total assigned
Still 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 placement, queue wait, and outcome at the current simulated time
Request / clientTargetQueue waitState

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.

Vertical scaleBigger server: more CPU/RAM per instanceCPURAMupgrade
Horizontal scaleMore server instances, shared workLoad balancerNode 1Node 2Node 3Add nodes until cost or coordination limits you

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.

1.DNS / GSLBSend users toward a nearby or healthy region before packets reach your datacenter.
2.L3 ECMP / anycastSpread packets across equal-cost paths or POPs (edge sites) using one shared IP.
3.L4 NLBRoute TCP/UDP flows by IP and port without decoding HTTP bodies.
4.L7 ALB / gatewayInspect URLs, headers, or cookies to steer traffic, terminate TLS, or run WAF rules.
5.Service mesh data planeSidecars balance east-west RPC with retries, timeouts, and circuit breaking.
6.App-level clientsSDKs pick backends for gRPC or REST when you control both ends.

The table maps the same idea onto the hops a typical request crosses inside one region, from the edge down to the database.

HopTypical balancer
Edge → CDNGeoDNS, anycast
Edge → frontendL4 NLB (high conn/sec)
Frontend → APIL7 application proxy
API → serviceMesh proxy (least-request, outlier detection)
Service → DBDatabase proxy or connection pooler
Service → cacheConsistent 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.

Do you own and ship the client code?

No

Yes

Latency budget allows periodic discovery?

No

Yes

Origin list mostly static and stale lists OK?

No

Yes

Need rich L7 rules at edge without a proxy?

Yes

No

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.

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
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
    end

L7 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.

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
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 weights

The 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.

QuestionPractical 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 TCPL4 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.

CheckCalculationDesign implication
Normal pool1.2K peak QPS ÷ 13 replicas~92 QPS per replica
Safe test limitp99 starts rising above 120 QPS/replicaScale before this, around 90-100 QPS/replica
One zone lost13 replicas placed 5 / 4 / 4; lose the zone with 58 survive: 1,200 ÷ 8 = 150 QPS each, above the assumed safe limit
Retry storm1.2K QPS × (1 + 2 retries)Up to 3.6K attempted QPS
Health check loadtargets × balancer nodes ÷ intervalUse 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.

Timeouts?

Client retry 1

Client retry 2

Normal pathRetry amplificationGuards

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 shapeLayerTypical use
Application load balancerL7 HTTP/HTTPS/gRPC/WebSocketPath/host routing, TLS termination, slow starts, edge security rules.
Network load balancerL4 TCP/UDP/TLS passthroughStatic IPs, preserve client IP, extreme conn/sec, non-HTTP protocols.
Gateway load balancerGENEVE (a tunnelling protocol) to appliancesInline firewalls/IDS inspection without hairpinning through a proxy VM fleet manually.
Classic load balancerLegacy mixOlder stacks; prefer ALB/NLB for new designs.
DNS / edge routingDNS / edgeGeo 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)

1. Determine the viewer region from resolver hints
2. Filter to datacenters that are healthy and have capacity
3. Score the survivors by latency or business weights
4. Return IPs with a TTL (time to live, how long the answer may be cached; often tens of seconds)

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.

AlgorithmPicks byInterview fit
Round robinRotate equallyShort, similar HTTP requests on homogeneous pods.
RandomUniform pick (or power-of-two)Huge pools; cheap local decisions without global least-conn state.
Least connectionsFewest open sockets / in-flightSSE and WebSockets especially: long-lived streams skew load; new upgrades should prefer lighter nodes.
Least response timeRecent latency / outstanding workVariable request cost; avoid feeding a slow replica.
IP hash (affinity)Hash client IP → backendCrude 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.

SignalProvesRisk
TCP connectProcess is listeningApp may be wedged while port is open
HTTP readinessShallow request succeedsDB dependency may still be broken
Dependency-aware readinessReal traffic path likely worksToo strict checks can cascade-remove entire pools
Passive outlier detectionGray 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.

  1. Remove the instance from rotation so the balancer stops scheduling new connections to it.
  2. Wait for in-flight HTTP and RPC calls to finish rather than cutting them off.
  3. Honor long-lived sockets: WebSockets and gRPC GOAWAY (the graceful shutdown frame) need time to close.
  4. 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

  1. The current (blue) pool serves all traffic while nothing else changes.
  2. Deploy the new (green) pool in parallel; it receives no user traffic yet.
  3. Smoke test it privately before any user traffic is routed there.
  4. Switch the routing policy for new requests to green; allow old in-flight work and long-lived connections to drain under a defined deadline.
  5. Keep the old pool warm so rollback is another flip, not a redeploy.

  1. Route 1-5% of traffic to the new version behind an LB weight.
  2. Watch errors, latency, and business KPIs (the key business metrics) for that slice.
  3. 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.

Active-passive pairThe standby takes over the via heartbeat protocols; simple, but the standby's capacity sits idle.
Active-active + ECMPEvery node serves traffic; routing protocols (ECMP, equal-cost multipath) heal member loss quickly.
Managed cloud LBThe vendor hides replicas across zones; you configure listeners and targets.

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.

BalancerTalk track
Managed L7 LBHigh HTTP throughput per AZ; terminates TLS and routes paths.
Managed L4 LBMillions of flows/sec class hardware path for TCP/UDP.
Software proxiesSoftware LBs tuned on bare metal stay relevant at huge QPS.
Service mesh proxyRich observability and policies per service instance.
Consistent-hash L4 designL4 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

  1. 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.
  2. 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.
  3. 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.