TL;DR

1. The problem: why load balance?

Assume Instagram's API sees ~600,000 requests per second at peak (an assumption — see Capacity estimation), and one application server can comfortably handle ~1,000 requests per second. You need at least 600 servers just for the API tier — more with headroom. That raises immediate questions:

A load balancer answers all four. It is the component that makes horizontal scaling (more machines rather than a bigger machine — see Core principles) work in practice. It shows up in the Instagram diagram right after DNS, and — less visibly — between almost every pair of tiers inside the data center.

🧠 Analogy Picture a busy airport check-in hall with 40 counters. Instead of letting passengers pick a counter at random (and form one huge queue at counter 1), an agent at the entrance directs each passenger to a counter that's open and has a short queue. If a counter's computer crashes, the agent stops sending people there. When the shift changes, a counter stops accepting new passengers but finishes the ones already in line (draining). If it gets busy, management opens more counters (autoscaling). And because one agent could faint, there's a second agent ready to take over (redundant LBs).

2. What a load balancer does

📱 Client A 📱 Client B 💻 Client C Load balancer VIP 203.0.113.10:443 algorithm + health checks TLS termination Server 1 ✔ (healthy) Server 2 ✔ (healthy) Server 3 ✔ (healthy) Server 4 ✖ (ejected) health ✖
Clients only know the VIP. The load balancer picks a healthy backend for each connection or request and silently skips ejected servers.

⚖️ Distribute load

Spread connections/requests across a pool using an algorithm, so capacity scales with the number of servers.

❤️ Health checking

Detect failed or slow backends and stop sending them traffic — turning server failures into non-events.

🎭 Abstraction (VIP)

Clients see one stable virtual IP/hostname; servers can be added, removed or replaced behind it.

🔐 Offload

Terminate TLS, compress, enforce timeouts and limits, add headers like X-Forwarded-For — so app servers don't have to.

🚦 Traffic control

Drain servers for deploys, shift percentages for canaries, route by path to different services, shed load when overloaded.

📈 Observability

A central point to measure request rates, latencies and error codes per backend.

3. Layer 4 vs Layer 7 load balancing

"L4" and "L7" refer to the OSI layers the load balancer looks at (see Networking basics).

3.1 L4 (transport-level) load balancers

An L4 LB sees only the IP and TCP/UDP headers — source/destination IP and port. It picks a backend when a connection starts (usually by hashing the 5-tuple) and then forwards every packet of that connection to the same backend. It never decrypts or parses the payload, so it can't see URLs or cookies, but it is extremely fast (millions of packets per second per machine), works for any protocol (HTTP, gRPC, MQTT, databases, QUIC) and adds microseconds of latency.

Ways an L4 LB forwards packets:

3.2 L7 (application-level) load balancers

An L7 LB terminates the client's connection (including TLS), parses the HTTP request, and then opens or reuses a separate connection to a backend. Because it understands requests, it can:

The price: more CPU and memory per request (TLS crypto, HTTP parsing), more latency (sub-millisecond to a few ms), and the LB must understand the protocol.

L4 load balancerL7 load balancer
Looks atIPs, ports, protocol (5-tuple)Full HTTP: method, path, headers, cookies, body
Unit of balancingConnection (or flow)Individual request
TLSPasses it through untouchedUsually terminates it
Routing intelligenceLow: hash / round robin / least connectionsHigh: path/host/header routing, canaries, A/B, retries
PerformanceVery high (millions of pps; kernel bypass/XDP possible)Lower per box (thousands to tens of thousands of RPS per core)
Latency addedMicrosecondsSub-ms to a few ms
Protocol supportAnything over TCP/UDPHTTP/1.1, HTTP/2, gRPC, WebSocket (protocol-specific)
Sees client IP?Yes (DSR/tunnel) or via PROXY protocolPasses it on in X-Forwarded-For
ExamplesKatran, AWS NLB, Google Maglev, LVS/IPVS, HAProxy (TCP mode)Proxygen, Envoy, NGINX, HAProxy (HTTP mode), AWS ALB
Instagram tierFirst hop at the edge and DC: spreads packets across L7 proxiesTerminates TLS/QUIC, routes HTTP to the right region and service

4. Load-balancing algorithms

The algorithm decides which healthy backend gets the next connection or request. There's no universally best one — it depends on whether requests are uniform, whether servers differ in size, whether state lives on specific servers, and how many independent LB instances are making decisions at the same time.

4.1 Round robin

Send requests to servers in turn: 1, 2, 3, 1, 2, 3… Simple, stateless and perfectly fair if all servers are equal and all requests cost the same. It ignores load: if request #4 is a slow 2-second search query and #7 is a 5 ms "like", server 1 can pile up slow work while round robin keeps feeding it.

4.2 Weighted round robin

Give each server a weight proportional to its capacity. With A (weight 5, a big new machine), B (3) and C (2), out of every 10 requests A gets 5, B 3 and C 2. A good implementation (e.g. NGINX's "smooth" weighted round robin) interleaves them rather than sending A five in a row:

weights: A=5, B=3, C=2   (total 10)
each step: add every server's weight to its running score, pick the highest, subtract the total from the winner
sequence for 10 requests:  A B C A A B A C B A    → A×5, B×3, C×2, nicely spread

Weights are also used for slow start (a freshly started server with a cold cache gets weight 1, ramping to full over a minute) and canaries (new version gets 1% of traffic).

4.3 Least connections

Send the new connection to the server with the fewest active connections. If A has 12, B has 7 and C has 9, B gets it. This adapts automatically to slow requests and uneven work: a server stuck on slow requests accumulates connections and receives fewer new ones. Weighted least connections divides by weight (A: 12/5 = 2.4, B: 7/3 ≈ 2.3, C: 9/2 = 4.5 → B). It's the natural choice for long-lived connections (WebSockets, DM gateways — section 13), where round robin leads to imbalance over time.

4.4 Least response time (latency-aware)

Prefer the server with the lowest recent latency, typically an exponentially weighted moving average (EWMA) of response times, often combined with outstanding requests: score = ewma_latency × (active_requests + 1). If A averages 40 ms with 3 in flight (score 160) and B averages 25 ms with 5 in flight (score 150), B wins. This automatically avoids servers that are slow because of a noisy neighbour, garbage-collection pauses or a bad disk. Risk: latency measurements are noisy, and a server that fails fast (returning errors in 1 ms) looks "fastest" — so combine with error-aware outlier ejection.

4.5 IP hash (source hashing)

server = hash(client_ip) mod N. The same client IP always lands on the same server — a cheap form of stickiness without cookies, usable even at L4. Problems: thousands of users behind one carrier NAT all land on one server (hotspot); a phone changing networks changes IP and therefore server; and when N changes, almost everyone moves:

N = 4 → 5 servers with mod-N hashing:
a key stays put only if hash mod 4 == hash mod 5  → about 1 in 5 keys
→ ~80% of clients are remapped (and lose any server-local state or cache)

4.6 Consistent hashing

Consistent hashing places servers and keys on a hash ring; each key goes to the next server clockwise. Adding or removing one of N servers moves only ~1/N of the keys. It's used when affinity matters: routing the same user/key to the same cache server (keeping its local cache hot), or keeping the same connection on the same backend across many L4 LB instances. Full details, including virtual nodes, are in Consistent hashing.

 hash space: 0 ──────────────────────────────────────────────────────────────── 2^32 (wraps to 0)
                  S1          k1      k2        S2             k3        S3          k4
                  ●           ○       ○         ●              ○         ●           ○
 rule: each key goes to the first server to its RIGHT (wrapping around the end)
       k1, k2 → S2      k3 → S3      k4 → S1 (wraps)
 remove S2           → only k1, k2 move (to S3); k3 and k4 are untouched
 add S4 between k1,k2 → only k1 moves (to S4); everything else stays put
 (with mod-N hashing, the same change would reshuffle almost every key)

Google's Maglev and Meta's Katran use a Maglev-style consistent hashing table so that many independent L4 LB machines, each seeing a different subset of packets (because routers spread packets across them), all pick the same backend for a given connection — and so that adding or removing a backend disturbs as few existing connections as possible.

4.7 Power of two choices (P2C)

Least connections needs an accurate, global view of load. But a large system has many LB instances (or client-side balancers in every service), each with its own partial, slightly stale view. If all of them send to "the least-loaded server", they all pick the same server at the same moment and overload it — herd behaviour. Pure random choice avoids herding but produces noticeable imbalance.

Power of two choices splits the difference: pick two servers at random, send the request to the less loaded of the two.

function pick(servers):
    a = random_choice(servers)
    b = random_choice(servers)          # distinct from a
    return a if load(a) <= load(b) else b   # load = active requests, or EWMA latency × active

The maths is striking: with n servers, the busiest server under random assignment carries roughly log n / log log n more than average; with two random choices it drops to roughly log log n — an exponential improvement from checking just one more server. And because each LB samples different random pairs, they don't herd. Envoy's default "least request" balancer, Finagle (Twitter) and NGINX's random two least_conn all use P2C. It's an excellent default for service-to-service traffic with many independent balancers.

AlgorithmState neededHandles uneven requests?Affinity?Best for
Round robinA counter❌❌Identical servers, uniform short requests
Weighted round robinWeights + counters❌❌Mixed hardware, canaries, slow start
Least connectionsActive connection counts✅❌Long-lived connections, variable request cost
Least response timeLatency EWMA + counts✅✅❌Heterogeneous or noisy backends
IP hashNone (just hashing)❌✅ (fragile)Simple stickiness at L4; legacy stateful apps
Consistent hashingRing / lookup table❌ (needs bounded-load variant)✅ stable under membership changesCache affinity, multi-instance L4 LBs, sharded backends
Power of two choicesPer-backend load (local view is fine)✅❌Many independent balancers, microservices, client-side LB

5. Health checks and outlier ejection

A load balancer is only as good as its knowledge of which backends are healthy. Sending 1/600th of traffic to a dead server means 0.17% of requests fail — at 600K QPS, that's 1,000 errors per second.

5.1 Active health checks

The LB periodically probes each backend: a TCP connect (L4), or an HTTP request such as GET /healthz expecting 200 (L7). Typical configuration:

interval: 5s            # probe every 5 seconds
timeout: 2s             # no answer within 2s = failure
unhealthy_threshold: 3  # 3 consecutive failures → mark DOWN   (~15 s to detect)
healthy_threshold: 2    # 2 consecutive successes → mark UP again
path: /healthz          # cheap endpoint: "process is up and ready to serve"

5.2 Passive health checks

The LB watches real traffic: connection failures, timeouts, 5xx responses, resets. Passive checks detect problems instantly on the request path and catch failures that a trivial /healthz misses (e.g. the server returns 200 for health checks but 500 for real requests because a config file is corrupt).

5.3 Outlier ejection

Outlier detection uses passive signals to temporarily remove misbehaving backends. Envoy-style rules, for example:

💡 Panic mode If most backends look unhealthy, the problem is probably the health check or a shared dependency, not the servers. Many LBs have a panic threshold: if healthy hosts fall below, say, 50%, ignore health status and spread traffic across all hosts — better to serve some requests successfully than to crush the few "healthy" survivors with 100% of the load. This is the same lesson as Meta's 2021 DNS outage (see DNS): safety mechanisms must not all fail closed at once.

6. Session persistence (sticky sessions)

Sticky sessions (session affinity) mean the LB sends all requests from the same user to the same backend. Implementations: an LB-issued cookie naming the backend (SERVERID=app-17), consistent hashing on a user ID or session cookie, or IP hash at L4.

Why would you want it? Because the server holds something about the user in its own memory — a server-side session, a shopping cart, a partially uploaded file, a warm per-user cache. Why it's a problem:

The standard fix — and the reason the Instagram diagram labels the application tier "stateless" — is to move state out of the servers:

Legitimate uses of affinity remain: long-lived connections (a WebSocket is inherently tied to one server for its lifetime), cache locality (consistent hashing users to cache nodes improves hit ratio — "soft" affinity where any node still works if the preferred one is down), and per-connection protocol state at L4 (all packets of a TCP or QUIC connection must reach the same backend).

7. TLS termination

Where should HTTPS be decrypted? Three options:

ModeHowProsCons
Termination at the LBLB decrypts; talks plain HTTP to backendsBackends are simpler and cheaper; L7 routing possible; certificates managed in one place; hardware/crypto offloadTraffic inside the network is unencrypted (unacceptable for many security models today)
Termination + re-encryptionLB decrypts, inspects/routes, then opens a new TLS (often mutual TLS) connection to the backendL7 features and encryption in transit everywhere; backends authenticate the LBTwo crypto operations; internal certificate infrastructure required
TLS passthroughL4 LB forwards encrypted bytes; backend terminatesEnd-to-end encryption; LB never holds keysNo L7 routing, retries or header injection at the LB; every backend needs certs

Big consumer platforms terminate client TLS (and QUIC) at the edge L7 proxies, as close to the user as possible, because handshakes cost round trips (see Networking basics). The edge then re-encrypts traffic over long-lived connections across the backbone to the data center. Using SNI, one LB fleet can serve certificates for many hostnames. TLS termination is a serious CPU cost at scale — modern CPUs with AES instructions and session resumption keep it manageable — and the private keys on these machines are among the most sensitive secrets in the company (see Security).

8. Connection draining and deploys

Deploying new code means restarting every server, typically in batches (a rolling deploy). Simply killing a process would abort its in-flight requests — at 1,000 requests/second per server with 100 ms average latency, ~100 requests are in progress at any moment. Connection draining (AWS calls it "deregistration delay") removes a server gracefully:

  1. Mark draining: the deploy system tells the LB (or the server starts failing its readiness check) that this backend should get no new connections or requests.
  2. Finish in-flight work: existing requests complete normally. For HTTP/2 and HTTP/3, the server sends a GOAWAY frame so clients open new streams on another connection; HTTP/1.1 responses carry Connection: close.
  3. Wait with a deadline: e.g. up to 30 s for API servers. Anything still open after the deadline is closed (clients retry idempotent requests).
  4. Stop, upgrade, start: deploy the new version and wait for it to warm up (load config, open DB pools, fill caches).
  5. Re-enable: readiness passes → the LB adds it back, ideally with slow start (gradually increasing weight) so a cold server isn't flooded.

Draining long-lived connections (WebSockets, DM gateways, MQTT) is harder because they may last hours: the server instead tells clients to reconnect (a "please reconnect" control message or close frame) gradually and with random jitter, so thousands of clients don't reconnect in the same second. Section 13 covers this.

⚠️ The LB itself also needs deploys Restarting a load balancer process drops every connection it holds. Production proxies support hot restart / socket takeover: the new process inherits the listening sockets from the old one, which drains gracefully. Meta has publicly described doing exactly this for its Proxygen and Katran fleets so that releasing the load balancers themselves is invisible to users.

9. Avoiding the load balancer as a single point of failure

We added an LB so that no single server failure takes the site down — but now the LB is a single box everything passes through. If it fails, everything fails. Three common patterns, often combined:

① Active–passive pair ② Several LBs behind DNS ③ Anycast + ECMP floating VIP LB-Aactive ✔ LB-Bstandby VRRP heartbeats B claims the VIP if A dies (~1–3 s); half capacity idle DNS: IP1, IP2, IP3 LB 1IP1 LB 2IP2 LB 3IP3 all active; failover bounded by DNS TTL + health checks router (ECMP) LBVIP LBVIP LBVIP all announce the same VIP; BGP drops a dead LB in seconds
Three ways to make the load-balancing tier itself redundant. Large systems combine them: anycast/ECMP across many L4 machines per site, several sites returned by DNS.

9.1 Active–passive pair with a floating IP (VRRP)

Two LB machines share a virtual (floating) IP. The active one owns it; the standby monitors the active using VRRP (Virtual Router Redundancy Protocol — implemented by tools like keepalived) heartbeats. If heartbeats stop, the standby claims the IP (announcing it with a gratuitous ARP so the network switches send traffic to it) within a second or a few. Simple and classic for small/medium deployments. Downsides: half the hardware sits idle; capacity is capped at one machine; connection state is lost on failover unless synchronised; and a network partition can cause split brain (both think they're active).

9.2 Active–active: multiple LBs behind DNS

Run several independent LBs (or LB clusters), each with its own IP, and return all of them from DNS (multi-value or round-robin records, with health checks). All capacity is used. Failover depends on DNS TTLs and on clients trying another IP — so it's best for coarse failover between sites, not for instant recovery of one box.

9.3 Active–active: anycast and ECMP

Many LB machines announce the same VIP to the upstream routers via BGP. Routers use ECMP (Equal-Cost Multi-Path) to spread flows across all of them by hashing each packet's 5-tuple, so a given connection's packets consistently reach the same LB machine. If an LB dies, its BGP session drops and routers stop sending to it within seconds; because all LBs use the same consistent-hashing table to pick backends, the connections that move to another LB machine still reach the same backend. The same VIP can also be announced from many PoPs worldwide (anycast), so users reach the nearest site. This is how Katran, Google's Maglev and cloud NLBs scale out "horizontally" with no single LB box in the path.

10. Global vs local load balancing, and LB tiers

10.1 GSLB vs local load balancing

Global server load balancing (GSLB)Local load balancing
ChoosesWhich region / data center / PoPWhich server inside one site
MechanismDNS (geo/latency/weighted answers), anycast BGPL4/L7 LBs, client-side LB in services
SignalsUser location, network latency, site health and capacity, data residencyServer health, connections, latency, request content
Reaction timeSeconds (anycast) to minutes (DNS TTL)Milliseconds to seconds
GranularityPer resolver / network prefixPer connection or per request

GSLB is covered in depth in DNS and Multi-region. A global platform also needs capacity-aware global balancing: if the Mumbai PoP is full during an evening peak, some Indian users should spill over to Singapore rather than queue — the global map is recomputed continuously from load data.

10.2 Tiers: edge L4 → L7 → service

At large scale, load balancing isn't one box but a pipeline of tiers, each doing what it's best at:

Edge PoP (near the user, e.g. Mumbai) Data-center region 📱 AppQUIC / HTTPS GSLB (DNS)picks PoP / region L4 LB (Katran)anycast VIP, ECMP L7 proxy (Proxygen)TLS / QUIC ends here L4 LB (Katran)VIP per cluster L7 (Proxygen)route to service pool API Gatewayauth, rate limits Feed service ×Nclient-side LB (P2C) ① lookup ② connect to PoP VIP ③ private backbone, warm encrypted connections
Instagram-style load-balancing tiers. DNS picks the PoP; an anycast L4 tier spreads packets over L7 proxies; the L7 proxies terminate TLS/QUIC and forward over the backbone to a region, where another L4/L7 layer, the API gateway and client-side balancing inside services finish the job.

11. Hardware vs software load balancers

Hardware appliancesSoftware on commodity serversManaged cloud LBs
ExamplesF5 BIG-IP, Citrix NetScaler (ADC), A10HAProxy, NGINX, Envoy, Katran, Proxygen, LVS/IPVSAWS ALB / NLB / GWLB, Google Cloud LB (Maglev-based), Azure Load Balancer / Front Door
PerformanceVery high per box (custom ASICs/FPGAs)High, scales out horizontally; kernel-bypass (XDP/DPDK) reaches millions of pps per serverScales automatically, invisible to you
Cost modelExpensive capex + licences; buy for peakCommodity hardware; pay for engineering timePay per hour + per GB/connection
FlexibilityVendor features only; slow upgradesFully programmable, open source, deploy like any codeLimited to provider features
ScalingScale up (bigger box), active-passive pairsScale out with anycast/ECMPElastic
Typical userTraditional enterprises, banks, telcosLarge internet companies (Meta, Google, Netflix…), Kubernetes usersMost startups and cloud-native companies

The industry trend at hyperscale is clear: software load balancers on commodity servers. Google publicly described Maglev (2016), and Meta open-sourced Katran (2018), an L4 load balancer built on the Linux kernel's XDP and eBPF that processes packets at the network driver level — before the normal kernel networking stack — so a single commodity server can forward millions of packets per second. At L7, Meta uses its open-source Proxygen HTTP framework as its edge and data-center reverse proxy, with HTTP/2 and HTTP/3 support. For almost everyone else, a managed cloud LB or HAProxy/NGINX/Envoy is the right answer.

12. Autoscaling integration

Load balancers and autoscalers work as a team: the autoscaler decides how many servers exist; the LB decides which one gets each request. The integration points:

  1. Metrics: the LB reports requests per second per target, latency and errors; together with CPU, these drive scaling policies, e.g. "keep ~700 RPS per instance" (target tracking).
  2. Scale out: new instances boot and register in the LB's target group (or service discovery). They join only after passing readiness checks, then receive gradually increasing traffic (slow start) while caches and JIT warm up.
  3. Scale in: the instance is first deregistered and drained (section 8), then terminated — never the other way round.
  4. Headroom: scaling takes minutes (boot, warm-up), so keep spare capacity for sudden spikes and use predictive/scheduled scaling for known daily peaks.
Assume evening peak 600K RPS, target 700 RPS/instance (70% of the 1,000 RPS max for headroom):
  needed = 600,000 / 700 ≈ 858 instances
Night trough 150K RPS → 150,000 / 700 ≈ 215 instances  → ~640 instances released for ~10 hours/day
If a region fails and its traffic shifts here (+50%), you need ~1,290 instances —
  either pre-provisioned headroom or the ability to scale out within minutes.
⚠️ Autoscaling pitfalls Scaling on CPU alone can mislead (an I/O-bound service may be overloaded at 30% CPU). New instances with cold caches can make latency worse at first. And if the backing database is the bottleneck, adding app servers just adds more pressure on it — autoscaling the stateless tier must be paired with capacity planning for the stateful tiers.

13. Load balancing long-lived WebSocket connections

Most load-balancing intuition assumes short requests. Real-time features — Instagram's Direct messaging, typing indicators, live notifications — use connections that stay open for minutes or hours (WebSocket, MQTT). That changes the problem:

 Before deploy (least-connections):      During gradual drain of GW-2:          After
 GW-1 ███████████ 400K                   GW-1 ████████████████ 600K             GW-1 ████████ 300K
 GW-2 ███████████ 400K   send "reconnect"GW-2 ████ 120K (draining, jittered)    GW-2 ████████ 300K (new version)
 GW-3 ███████████ 400K   to 2% per 10 s  GW-3 ███████████████ 480K              GW-3 ████████ 300K
                                                                                 GW-4 ████████ 300K (autoscaled)

14. How Instagram's request passes through the LB tiers

Follow a single "load my feed" request from a phone in Mumbai:

📱 AppMumbai
→DNS
Edge L4Katran, anycast
→IPIP, hash
Edge L7Proxygen, TLS ends
→backbone
DC L4 → L7pick cluster
→
Gateway → FeedP2C to instance
  1. GSLB: Meta's authoritative DNS maps the user's network to a nearby edge PoP (Mumbai) based on latency, capacity and health, returning that PoP's VIP. See DNS.
  2. Routers + ECMP: the VIP is announced via BGP by many Katran machines in the PoP; the PoP's routers hash each packet's flow to one of them.
  3. Edge L4 (Katran): looks up the connection (or QUIC connection ID) in its consistent-hash table and encapsulates the packet (IP-in-IP) to one of many Proxygen L7 machines. Responses go straight back to the client (DSR), so Katran only handles the small inbound half of traffic.
  4. Edge L7 (Proxygen): terminates QUIC/TLS a few milliseconds from the user, parses the HTTP request, and forwards it over a long-lived, pre-warmed connection across Meta's backbone to the best data-center region for this request.
  5. Data-center L4 → L7: the same two-tier pattern again inside the region: an L4 tier spreads connections over an L7 proxy fleet, which routes by host/path to the right backend cluster with health checks, outlier ejection and retries.
  6. API gateway → service: the gateway authenticates the token, applies rate limits and routes to the Feed service; inside the service mesh, each hop (Feed → Graph, Feed → Redis) uses client-side load balancing. Media URLs in the response send the app to the CDN, a separate path with its own L4/L7 load balancing.
📸 Instagram specifics (public information)

Rough numbers

Assume 600K API RPS at peak worldwide, spread over ~30 edge PoPs → ~20K RPS per PoP on average
  (big PoPs like Mumbai much more, maybe 60–100K RPS)
L4 tier: a Katran-style XDP box can forward millions of packets/s → a handful per PoP suffices
L7 tier: assume ~5–10K TLS/QUIC RPS per proxy server → 10–20 proxies for a 100K RPS PoP (+ headroom)
Per-region app tier: 600K RPS / 3 regions = 200K RPS ÷ 700 RPS per instance ≈ 290 instances per region
If a 1-in-600 dead server isn't ejected: 0.17% errors ≈ 1,000 failed requests per second globally

15. Trade-offs & common pitfalls

✅ What load balancing gives you

  • Horizontal scalability behind one stable address
  • High availability: failed servers removed automatically
  • Zero-downtime deploys via draining and slow start
  • Central place for TLS, routing, limits and metrics
  • Elasticity with autoscaling

⚠️ Costs

  • Extra hop: latency (µs at L4, ~ms at L7) and cost
  • The LB tier must itself be redundant and scaled
  • L7 termination concentrates sensitive TLS keys and CPU load
  • Misconfigured health checks can eject everything
  • Long-lived connections and stateful services resist balancing

Common pitfalls

16. Real-world technologies

TechnologyLayerWhen to use it
HAProxyL4 & L7Battle-tested, very fast software LB; rich health checks and stick tables
NGINXL7 (and L4 stream)Reverse proxy + web server + LB; ubiquitous; simple config
EnvoyL7 (and L4)Modern proxy with dynamic config (xDS), P2C, outlier detection, HTTP/2/3, gRPC; data plane of service meshes (Istio)
Katran (Meta)L4XDP/eBPF L4 LB with consistent hashing, IPIP + DSR, anycast/ECMP; hyperscale edges
Proxygen (Meta)L7C++ HTTP/1.1, HTTP/2, HTTP/3 library and proxy used at Meta's edge and DCs
Google Maglev / Cloud Load BalancingL4 (+ L7 on top)Google's software L4 LB; powers GCP's global anycast load balancers
AWS NLB / ALB / GWLBL4 / L7 / L3 appliancesNLB for TCP/UDP/TLS at very high scale and static IPs; ALB for HTTP routing, WebSockets, gRPC; GWLB for inserting firewalls
LVS / IPVS, keepalivedL4; VRRP failoverLinux kernel L4 balancing; active-passive floating IPs
F5 BIG-IP, Citrix ADCL4–L7 hardware/virtual appliancesEnterprises needing vendor support and appliance features
Kubernetes Service / Ingress, gRPC client-side LBL4 / L7 / client-sideBalancing inside clusters and between microservices

17. Interview corner

L4 or L7 load balancer for Instagram's API — and why not just one?

Both, in tiers. L7 is needed for the API: per-request balancing (HTTP/2 multiplexes many requests per connection), path-based routing to services, TLS/QUIC termination near users, retries, timeouts, header injection and per-request metrics. But L7 is CPU-heavy, so a single L7 box doesn't scale and is a SPOF. Put a fast, stateless L4 tier (anycast + ECMP + consistent hashing, e.g. Katran/Maglev/NLB) in front to spread connections across a horizontally scalable L7 fleet. Inside the DC, repeat the pattern and use client-side balancing between services.

Which load-balancing algorithm would you choose for (a) stateless API servers, (b) DM WebSocket gateways, (c) a fleet of cache servers?

(a) Least-request / power-of-two-choices (or least response time) — handles variable request cost and works well with many independent LB instances; round robin is acceptable if requests are uniform. (b) Least connections (or P2C on connection count/memory), because connections are long-lived and the load is the number of open connections. (c) Consistent hashing on the cache key, so each key lives on one node and membership changes move only ~1/N of keys (with bounded-load variants or replication for hot keys).

How do you make sure the load balancer isn't a single point of failure?

Redundancy at every level: an active-passive pair with a floating IP via VRRP for small setups; at scale, many active L4 LBs announcing the same VIP via BGP with routers spreading flows via ECMP, all using the same consistent-hash table so connections stay pinned to the same backend if an LB dies; multiple sites/PoPs returned by DNS (GSLB) or announced via anycast; plus hot-restart and draining for the LB software itself, and health checks that can't take everything out at once.

Your service uses sticky sessions and one server is at 95% CPU while others are at 40%. What do you do?

Short term: lower that server's weight or move a slice of sessions off it (if the session can be recreated). Root cause: affinity is pinning heavy users (or a big NAT) to one server. Long term: make the service stateless — move session data to a shared store (Redis) or signed tokens, keep durable data in databases — then switch to least-request/P2C balancing with no affinity. If affinity exists for cache locality, use consistent hashing with bounded loads so overloaded nodes spill over.

How do you deploy new code to 800 API servers without dropping requests?

Rolling deploy in batches (e.g. 5–10% at a time). For each server: fail readiness / deregister from the LB so it receives no new requests; send GOAWAY/Connection: close; let in-flight requests finish up to a deadline (~30 s); stop, upgrade, start; warm up; pass readiness; re-add with slow start. Watch error rate and latency per batch (canary analysis) and roll back automatically if they regress. Keep enough capacity headroom that the missing batch doesn't overload the rest.

What's the difference between active and passive health checks, and why do you need both?

Active checks probe each backend on a schedule (TCP connect or /healthz) — they detect dead or not-ready servers even with no traffic, but only test what the probe tests and only every few seconds. Passive checks observe real traffic (errors, timeouts, resets) and react instantly to failures that probes miss, e.g. a server that answers /healthz but fails real requests. Outlier ejection uses passive signals to temporarily remove bad hosts, with a max-ejection percentage and panic threshold so a systemic issue doesn't empty the pool.

How would you load balance millions of long-lived WebSocket connections for Instagram DMs?

An L4 tier (or L7 with WebSocket support and long idle timeouts) in front of a fleet of connection gateway servers; route new connections by least connections/P2C on connection count; cap connections per server; keep a session registry (user → gateway) so other services can deliver messages; send heartbeats to keep NAT/LB state alive; on deploys or rebalancing, ask clients to reconnect gradually with jitter rather than dropping everyone at once; clients reconnect with backoff and resume from the last acknowledged message. Autoscale on connection count and memory, not just CPU.

18. Check your understanding

Weighted round robin with weights A=4, B=1. Out of 1,000 requests, how many go to each server?

Total weight is 5, so A gets 4/5 = 800 and B gets 1/5 = 200.

You use hash(user_id) mod N to route users to 10 cache servers and add an 11th. Roughly what fraction of users move? What about with consistent hashing?

With mod-N, a key stays only if h mod 10 == h mod 11 — about 1 in 11 — so ~91% of users move (and their cached data is effectively lost). With consistent hashing, only about 1/11 ≈ 9% move — the keys the new server takes over.

Health checks run every 5 s with a 2 s timeout and an unhealthy threshold of 3. A server hard-crashes. Roughly how long until the LB stops sending it new requests via active checks alone, and how can you do better?

Up to ~5 s until the next probe plus 3 consecutive failures ≈ ~15 s (each probe failing after up to 2 s). Passive checks / outlier ejection do better: connection failures on real requests can eject the host within a handful of failed requests (milliseconds to a second at high traffic), and L7 LBs can retry idempotent requests on another backend meanwhile.

gRPC clients connect through an L4 load balancer to 20 backends, but one backend is at 90% CPU while others are idle. Why?

gRPC uses long-lived HTTP/2 connections that multiplex all requests. An L4 LB balances connections, so if a few high-traffic clients' connections landed on the same backend, all their requests stay there for the connection's lifetime. Fix: L7 (per-request) balancing, client-side load balancing across all backends, or periodically recycling connections (max connection age).

Why does power of two choices avoid the "herd" problem that pure least-connections has with many independent load balancers?

With least-connections, every LB instance (with similar, slightly stale data) picks the same globally least-loaded server, so they all pile onto it simultaneously. With P2C, each instance compares a different random pair, so choices are spread out, while still steering away from the busier server of each pair — giving near-optimal balance without global coordination.

A floating-IP active-passive pair loses the network link between the two LBs, but both can still reach the internet. What can go wrong?

Split brain: the standby stops hearing heartbeats, assumes the active died and claims the VIP while the active still holds it. Both answer for the same IP, causing flapping, broken connections and confused switches. Mitigations: redundant heartbeat links, fencing/quorum (a third witness), and preferring active-active designs (ECMP/anycast) at scale.

You restart 500K-connection WebSocket gateway servers during a deploy by just killing them, one at a time. What happens, and what should you do instead?

Each kill drops ~500K clients at once; they all reconnect within seconds — a thundering herd hammering the LB, TLS handshakes and auth service, possibly overloading the remaining gateways (which then fail too). Instead, drain gradually: send reconnect hints to small batches with random jitter over several minutes, have clients reconnect with exponential backoff and jitter, route new connections by least connections, and resume message streams from the last acknowledged ID.

🎯 Key takeaways

Related lessons