Concept 1: Load Balancing
System Design Bible — File 01 of 10 Difficulty: ★☆☆☆☆ (Most Fundamental) Prerequisites: The four foundations unpacked in Section 0 below (TCP/IP, HTTP, client-server, DNS).
Table of Contents#
- Prerequisites — What You Must Understand First
- Architectural Definition & "The Why"
- The Recursive Sub-Concept Tree
- Deep-Dive Mechanics
- Routing Algorithms — Exhaustive Breakdown
- Layer 4 vs. Layer 7 — The Core Distinction
- Health Checks, Failover & Session Persistence
- Pros, Cons & Trade-offs
- Interview Diagnostic Framework (Symptom → Solution)
- Real Implementations & Product Landscape
- Real-World Engineering Scenarios
- Interview Gotchas & Failure Modes
- Whiteboard Cheat Sheet
0Prerequisites — What You Must Understand First#
Load balancing sits on top of six networking ideas. If any is fuzzy, the mechanics later (especially Layer 4 vs Layer 7, and the forwarding modes in §3.2) will feel like memorization instead of understanding. Here they are, from scratch. The last two — how a message is physically wrapped for transmission, and what a network card actually does with the bits — are the ones almost every load-balancing explanation skips, and they are precisely the ones you need in order to understand why Direct Server Return is even possible.
0.1Client–server model and the "request/response"#
The web works on a client–server model: a client (a browser, a phone app, another service) sends a request over the network to a server (a machine running your application), and the server sends back a response. A request is a small message ("GET me the page at /home"); a response is the data ("here's the HTML"). The server does work to produce that response — CPU, memory, maybe a database lookup — and it can only do a finite amount of that work per second before it's saturated. Load balancing exists entirely because one server can't handle all the requests, so we need many servers and something to spread requests across them. Hold this: a request is a unit of work a server must spend resources on.
0.2IP addresses and ports (the "where")#
Every machine reachable on a network has an IP address — a numeric label like 93.184.216.34 that identifies which machine to deliver packets to. On each machine, a port (a number 0–65535) identifies which program/service on that machine (web servers conventionally listen on port 80 for HTTP and 443 for HTTPS). So the pair **IP:port** (e.g., 93.184.216.34:443) is the full address of a specific service on a specific machine. This matters because a Layer 4 load balancer (§5) makes its decisions using only this addressing information — it sees "a connection from this IP:port to that IP:port" and nothing about the actual content. Understanding that IP:port is all Layer 4 knows is the key to the whole L4-vs-L7 distinction.
0.3TCP vs UDP, and what a "connection" is#
TCP (Transmission Control Protocol) is the reliable, ordered, connection-based transport most of the web runs on. Before data flows, the two sides perform a handshake (the famous SYN → SYN-ACK → ACK three-message exchange) to establish a connection; then bytes are delivered reliably and in order; then the connection is closed. A connection is thus a stateful, ongoing conversation between one client and one server — which is why a load balancer must send all packets of the same TCP connection to the same backend (you can't hand packet 1 to server A and packet 2 to server B, or the conversation breaks). UDP is the connectionless, fire-and-forget alternative (no handshake, no guaranteed delivery/order) used for speed-critical, loss-tolerant traffic (video, gaming, DNS). Load balancers handle both, but TCP's "keep a connection pinned to one backend" requirement shapes much of their design.
0.4HTTP and DNS (the "what" and the "phone book")#
HTTP (HyperText Transfer Protocol) is the application-level language spoken over a TCP connection: a structured text request with a method (GET, POST…), a path (/api/users), headers (metadata like Host:, Cookie:, Authorization:), and optionally a body. HTTPS is HTTP wrapped in TLS encryption. The crucial point for load balancing: this rich, meaningful information — URL, headers, cookies — lives inside the connection's payload, and reading it requires decrypting and parsing the HTTP message. A Layer 7 load balancer (§5) does exactly that, which is why it can route by URL or cookie, while a Layer 4 one (which only sees IP:port, §0.2) cannot. Finally, DNS (Domain Name System) is the internet's phone book: it translates a human name (api.example.com) into an IP address. When you point api.example.com at your load balancer's IP, DNS is what delivers every client to the load balancer's front door in the first place — so DNS and load balancing work hand in hand.
0.5Encapsulation — what a "message" physically looks like on the wire (frames, packets, segments)#
This is the prerequisite that unlocks §2.8 and §3.2, and it is almost always skipped. When your browser sends GET /api/users, that text does not travel across the network as text. It travels wrapped in layers of headers, like a letter placed in an envelope, which is placed in a shipping box, which is placed on a labelled pallet. Each network layer adds its own wrapper on the way down the sending machine's stack, and each layer strips its own wrapper on the way up the receiving machine's stack. This wrapping process is called encapsulation, and the un-wrapping is decapsulation.
Here is the actual shape of one HTTP request leaving your laptop, drawn from the outside in:
+---------------------------------------------------------------------------+
| ETHERNET FRAME (Layer 2 — the data-link layer) |
| dst MAC: aa:bb:cc:dd:ee:01 src MAC: 11:22:33:44:55:66 type: 0x0800 |
| +----------------------------------------------------------------------+ |
| | IP PACKET (Layer 3 — the network layer) | |
| | src IP: 192.168.1.20 dst IP: 1.2.3.4 proto: 6 (TCP) TTL: 64 | |
| | +-----------------------------------------------------------------+ | |
| | | TCP SEGMENT (Layer 4 — the transport layer) | | |
| | | src port: 44321 dst port: 443 seq/ack flags: PSH,ACK | | |
| | | +-----------------------------------------------------------+ | | |
| | | | PAYLOAD (Layer 7 — the application layer) | | | |
| | | | "GET /api/users HTTP/1.1\r\nHost: api.example.com\r\n..." | | | |
| | | +-----------------------------------------------------------+ | | |
| | +-----------------------------------------------------------------+ | |
| +----------------------------------------------------------------------+ |
| FCS (4-byte checksum trailer) |
+---------------------------------------------------------------------------+Now the vocabulary, which people use sloppily and which this file will use precisely:
- A segment is the Layer 4 unit: your application's bytes plus a TCP header. The TCP header is 20 bytes in its minimal form and carries the source port, the destination port, the sequence number (where these bytes sit in the overall byte stream, which is how the receiver reassembles them in order), the acknowledgement number (how much the sender has successfully received from the other side), the window size (how much more the receiver is currently willing to accept — the flow-control knob), and the flag bits (
SYNto open a connection,ACKto acknowledge,FINto close politely,RSTto abort rudely,PSHto say "deliver this to the app now"). If the protocol is UDP rather than TCP, the Layer 4 unit is called a datagram and its header is only 8 bytes: source port, destination port, length, checksum — no sequence numbers, no acknowledgements, no window, because UDP promises nothing. The load-balancing consequence: everything a Layer 4 load balancer is allowed to know lives in this header plus the IP header outside it. Ports and addresses, nothing else. - A packet is the Layer 3 unit: the segment plus an IP header. The IPv4 header is 20 bytes minimum and carries the source IP address (4 bytes), the destination IP address (4 bytes), the protocol number identifying what is inside it (6 = TCP, 17 = UDP, 1 = ICMP, 4 = IP-in-IP — remember that last one, it is the whole basis of tunneling mode in §3.2 D), the TTL (time to live), which every router decrements by one and which causes the packet to be discarded when it reaches zero so that routing loops die instead of circulating forever, and the fragmentation fields that matter for MTU (below). The IP header is what makes a packet routable across the world — routers read the destination IP, consult their routing tables, and forward the packet hop by hop.
- A frame is the Layer 2 unit: the packet plus an Ethernet header and a 4-byte FCS (Frame Check Sequence) trailer, which is a CRC32 checksum the receiving network card uses to detect corruption on the wire and silently drop damaged frames. The Ethernet header is 14 bytes: a 6-byte destination MAC address, a 6-byte source MAC address, and a 2-byte EtherType field saying what protocol is inside (
0x0800= IPv4,0x86DD= IPv6,0x0806= ARP — you will meet that last one in §2.8). A frame is the only thing that ever actually travels on a physical link. Packets and segments are conceptual contents; the frame is the physical envelope.
The critical asymmetry — and this is the single most important sentence in this prerequisite: the IP addresses in the packet header are written once by the sender and stay the same for the entire journey across the internet, but the MAC addresses in the frame header are rewritten at every single hop. Your laptop's frame is addressed to your router's MAC, not to the destination server's MAC — your laptop cannot possibly know that server's MAC, and doesn't need to. Your router strips that frame, reads the IP packet inside, decides the next hop, and wraps the packet in a brand-new frame addressed to the next router's MAC. This happens at every hop, perhaps fifteen times, until the last router on the destination's local network builds a final frame addressed to the destination server's actual MAC. The IP packet inside rode the whole way untouched.
Hold this, because DSR (§3.2 C) is entirely built on it: you can change where a frame is physically delivered — by rewriting its destination MAC — without changing a single byte of the IP addresses inside it. The receiving machine will get a packet that still says "this is addressed to the VIP," even though it was hand-delivered to a specific backend's network card. That loophole is the whole trick.
MTU and why encapsulation is not free. Each link has a MTU (Maximum Transmission Unit) — the largest payload a single frame may carry. Standard Ethernet MTU is 1500 bytes, meaning the IP packet inside a frame may be at most 1500 bytes; add the 14-byte Ethernet header and 4-byte FCS and the frame itself is up to 1518 bytes on the wire. Subtract the 20-byte IP header and the 20-byte TCP header and your application gets 1460 bytes of actual data per full-size segment — a number called the MSS (Maximum Segment Size), which the two ends announce to each other during the TCP handshake. Now the consequence for §3.2 D: if a load balancer wraps that already-full 1500-byte packet inside another 20-byte IP header (IP-in-IP tunneling), the result is 1520 bytes, which exceeds the MTU and must either be fragmented (split into two frames, doubling the packet count and creating a new failure mode — lose either fragment and the whole original packet is lost) or dropped with an ICMP "fragmentation needed" message that tells the sender to shrink. This is why every tunneling deployment involves MTU tuning — lowering the backend's advertised MSS to ~1440, or enabling jumbo frames (MTU 9000) inside the datacenter where you control every switch. "Encapsulation overhead" is not an abstract cost; it is these 20 bytes pushing you over a hard limit.
Hold this: frame wraps packet wraps segment wraps your data; IP addresses survive the whole trip, MAC addresses are rewritten every hop, and every extra wrapper eats into a fixed 1500-byte budget.
0.6The NIC — what a network card actually is and what it does with arriving bits#
A NIC (Network Interface Card / Controller) is the hardware that connects a machine to a network — historically a card you plugged into a PCI slot, today almost always a chip integrated on the motherboard, and in a cloud VM a virtual NIC presented by the hypervisor that behaves identically from the operating system's point of view. It matters here for three reasons: it owns the MAC address, it enforces the bandwidth ceiling that pushes you toward horizontal scaling and DSR, and its offload features explain why some load balancers are fast and others are not.
What it physically does. The NIC converts between the bits your operating system holds in memory and the electrical, optical, or radio signals on the medium. On transmit, the kernel hands it a fully-built frame; the NIC serializes it onto the wire and appends the FCS checksum. On receive, it recovers bits from the signal, checks the FCS, discards corrupted frames, and then applies its MAC filter: it looks at the frame's destination MAC address and asks "is this mine?" If the destination MAC is the NIC's own address, or the broadcast address ff:ff:ff:ff:ff:ff, or a multicast group the machine has joined, the frame is accepted; otherwise it is silently discarded in hardware, without ever waking the CPU. (The exception is promiscuous mode, in which the NIC accepts every frame it sees regardless of address — this is what tcpdump and Wireshark enable to capture traffic, and what a network tap or an intrusion detection sensor relies on.)
How an accepted frame reaches your application. The NIC writes the frame into a ring buffer in main memory by DMA (Direct Memory Access) — copying it directly without CPU involvement — and then raises an interrupt to tell the kernel "there is work." The kernel's driver strips the Ethernet header, sees EtherType 0x0800, and hands the IP packet to the IP layer. The IP layer checks the destination IP: is this address configured on any of my interfaces? If yes, it strips the IP header and hands the segment up by protocol number to TCP. TCP looks at the destination port, finds the socket that is listening on it, and appends the payload to that socket's receive buffer, where your application's read() finally picks it up. Every one of those four checks is a place a packet can be dropped, and knowing which check drops it is how you debug load-balancing problems: a frame with a foreign MAC dies at the NIC, a packet for an IP you don't own dies at the IP layer, a segment for a port nobody is listening on gets a TCP RST, and a segment for a healthy socket whose buffer is full triggers a zero-window stall.
Notice the second check carefully, because §2.8 depends on it: **the IP layer accepts a packet if the destination IP is configured on any interface, not necessarily the interface the packet arrived on.** A packet addressed to the VIP that physically arrives on eth0 is accepted just fine if the VIP happens to be configured on lo. That permissiveness is a deliberate Linux behaviour (the "weak host model"), and DSR is built directly on top of it.
The bandwidth ceiling — the physical limit that shapes the whole file. A NIC has a fixed maximum throughput: 1 Gbps on old commodity hardware, 10/25 Gbps typically today, 100 Gbps on high-end datacenter gear. That number is a hard wall, and it is one of the concrete meanings of "you cannot scale a single machine forever" from §1.2. Work it out: at 10 Gbps you can move roughly 1.25 GB/s. If your service streams 5 Mbps video, one 10 Gbps NIC saturates at about 2,000 concurrent viewers — no matter how much CPU or RAM you add. This is also exactly why the response path matters so much in §3.2: in a full-proxy or NAT design, every byte of every response crosses the load balancer's NIC, so the LB's NIC becomes the ceiling on the whole service. DSR exists to take that ceiling away by letting responses leave from the backends' NICs in parallel — twenty backends with 10 Gbps NICs give you 200 Gbps of egress instead of one LB's 10.
Offloads — why the same software is fast on one box and slow on another. Modern NICs perform work in silicon that the CPU would otherwise do per packet. Checksum offload computes the IP/TCP checksums in hardware. TSO/GSO (TCP/Large Segmentation Offload) lets the kernel hand the NIC one 64 KB buffer and have the NIC chop it into 1460-byte segments itself, cutting per-packet CPU cost by a factor of forty. LRO/GRO (Large/Generic Receive Offload) does the reverse on receive, merging many arriving segments into one big buffer before the kernel processes it. RSS (Receive Side Scaling) hashes each arriving flow's 5-tuple and distributes flows across multiple hardware receive queues, each bound to a different CPU core — without it, a single core handles all interrupts and becomes the bottleneck long before the wire does. These are the reason a software load balancer can push line rate on modern hardware; they are also why a load balancer that must modify packets (rewriting headers, terminating TLS) loses some offload benefits and costs more CPU per byte than one that only rewrites a MAC address.
Hold this: the NIC owns the MAC address, filters frames in hardware by destination MAC, accepts packets for any IP the machine owns on any interface, and imposes a fixed bandwidth ceiling that the entire load-balancer forwarding-mode taxonomy exists to work around.
1Architectural Definition & "The Why"#
1.1The Plain Definition#
A load balancer (LB) is a component — hardware appliance, software daemon, or managed cloud service — that sits between clients and a pool of backend servers ("the fleet" or "the backend pool") and distributes incoming network requests across those servers according to a defined policy. Its job is to ensure that no single server is overwhelmed, that capacity is used evenly, and that the failure of any one server is invisible to the end user.
Think of it as the maître d' of a restaurant. Diners (requests) arrive at the door. The maître d' (load balancer) does not seat everyone at table 1. He looks at which tables (servers) are free, which waiter (server) is least busy, and routes each party to the optimal table so the kitchen throughput stays maximal and no waiter collapses.
1.2"The Why" — What Forced Its Invention#
To understand why load balancing exists, you must understand the historical scaling wall that engineers hit.
The era of the single server (1990s): Early web applications ran on one machine. The web server, the application logic, and often the database all lived on a single box. This works beautifully until it doesn't. There are two hard limits you smash into:
- Vertical scaling ceiling. The traditional fix for "my server is slow" was scale up (vertical scaling): buy a bigger machine — more CPU cores, more RAM, faster disk. But this hits physics and economics:
- A single machine has a maximum socket count, maximum RAM slots, and maximum NIC bandwidth. You cannot buy a server with 10,000 CPU cores.
- The price curve is super-linear. A machine twice as powerful costs far more than twice as much. High-end "big iron" carries a massive premium.
- It is a single point of failure (SPOF). If that one glorious machine dies — a power supply fails, a kernel panic, a bad deploy — your entire service is down. 100% outage.
- The concurrency wall. A single server can only hold so many concurrent TCP connections and process so many requests per second (RPS) before latency explodes and the OS runs out of file descriptors, memory, or CPU.
The pivot to horizontal scaling. The industry realized the answer was not a bigger machine but more machines — scale out (horizontal scaling). Run 10, 100, or 10,000 commodity servers, each cheap and replaceable, all serving the same application. This solves the cost curve (commodity hardware is cheap and linear) and the SPOF problem (lose one of 100 servers, lose 1% of capacity, not 100%).
But horizontal scaling created a new problem: If I have 100 identical servers, how does a client know which one to talk to? You cannot hand every user a list of 100 IP addresses. You cannot let clients pick randomly — some servers would be swamped while others idle. You need a single, stable entry point that intelligently spreads traffic.
That entry point is the load balancer. It is the enabling technology of horizontal scaling. Without it, "just add more servers" is impossible. This is why load balancing is concept #1 — it is the foundation on which almost every other concept in this bible is built.
1.3The Problems Load Balancing Solves (Summary)#
| Problem | How LB solves it |
|---|---|
| Single point of failure | Traffic reroutes away from dead servers automatically |
| Capacity ceiling | Add servers behind the LB to grow capacity linearly |
| Uneven load | Routing algorithms spread requests evenly |
| Zero-downtime deploys | Drain traffic from a server, deploy, re-add it |
| Global latency | Geo-aware LBs route users to the nearest region |
| DDoS / traffic spikes | LB absorbs and can shed/throttle abusive traffic |
2The Recursive Sub-Concept Tree#
Per the strict rule of this bible: before we go deep, every secondary term that appears must be explained from scratch. Here are the foundational sub-concepts you must own before the mechanics make sense.
2.1Sub-Concept: The OSI Model (Layers 4 and 7)#
IP:port) and never looks inside. On the right the flap opens and the HTTP message is revealed, which is
exactly why an L7 balancer can send /api and /static to different pools and an L4 one cannot.The OSI model is a 7-layer conceptual framework describing how data moves across a network. For load balancing you only need two layers, but you must understand them precisely.
- Layer 4 — The Transport Layer. This is the layer of TCP and UDP. At this layer, the unit of data is the segment, and the addressing information available is the IP address and port number (source IP:port and destination IP:port). Crucially, at Layer 4 the device does not look inside the payload. It does not know if this is an HTTP request for
/loginor a video stream — it only sees "a TCP connection from 5.6.7.8:44321 to 1.2.3.4:443." It routes based on connection metadata only. - Layer 7 — The Application Layer. This is the layer of HTTP, HTTPS, gRPC, WebSocket. At this layer the device can read and understand the full application message: the URL path (
/api/v2/users), HTTP headers (Host:,Cookie:,Authorization:), the HTTP method (GET/POST), and even the body. It understands the meaning of the traffic.
Mnemonic: Layer 4 knows the envelope (who it's from, who it's to). Layer 7 can open the envelope and read the letter.
2.2Sub-Concept: Virtual IP (VIP)#
A Virtual IP (VIP) is a single IP address that the load balancer advertises to the world as the address of the service. Clients connect to the VIP. Behind the VIP, the LB maintains the list of real server IPs (sometimes called RIPs). The client never sees or knows the real IPs — it thinks it is talking to one machine at the VIP, when in reality it is being fanned out to a whole fleet. The VIP is what makes "one stable entry point for 100 servers" possible.
Why "virtual." An ordinary IP address is tied to one machine's network interface. A VIP is called virtual because it is deliberately not permanently bound to any one machine — it is an address the service owns, which some machine currently holds on the service's behalf and which can be moved to a different machine in seconds without any client noticing. That mobility is the entire point: it is what lets you replace a dead load balancer without a DNS change (§7.2), and it is why the failover mechanism is described in terms of who currently answers ARP for the VIP (§2.8.3) rather than in terms of rewiring anything.
How a machine actually holds a VIP. Concretely, on Linux the LB adds the address to its network interface (ip addr add 1.2.3.4/32 dev eth0) and answers ARP for it. The address now exists in two places: on the interface (so the IP layer accepts packets for it, §0.6) and in every neighbour's ARP cache (so frames get physically delivered to this machine, §2.8.3). Moving the VIP therefore means two operations: remove it from the old machine's interface, add it to the new one's, and broadcast a gratuitous ARP so every peer's cache is corrected immediately rather than after a multi-minute timeout. That is precisely what keepalived automates.
Three ways a VIP is made highly available, each with a different failover speed and cost:
- Active–passive with VRRP (§7.2): two LBs, one holds the VIP, the standby claims it on failure. Failover in seconds. Cost: half your load-balancing hardware is idle.
- Anycast: many machines in different locations all legitimately announce the same VIP to the internet via BGP (the internet's routing protocol, in which networks advertise "I can reach this block of addresses"), and each router forwards to whichever announcement is closest by its routing metric. Users are therefore absorbed by the nearest instance automatically, and if one instance withdraws its announcement, routers converge onto another in seconds — with no DNS change and no TTL to wait out. This is how CDNs and GCP's global LB work, and it is defined further in §10.2.
- ECMP (Equal-Cost Multi-Path): within one datacenter, several LB machines announce the same VIP to the same router, which hashes each flow's 5-tuple to pick one — hardware-level balancing of the balancers themselves (the Maglev design, §9.1). Cost: when the set of LBs changes, the hash's output changes for existing flows too, which is why Maglev-class systems pair ECMP with consistent hashing (§4.7) so that a reshuffled flow still lands on its original backend.
RIP (Real IP) is the counterpart term you will see in LVS documentation: the backends' actual addresses. LVS docs also use DIP (director IP — the LB's own address on the backend-facing side). Knowing this vocabulary is how you read the LVS manual without friction.
2.3Sub-Concept: Upstream / Backend Pool#
The upstream (Nginx terminology) or backend pool / target group (AWS terminology) is simply the named set of real servers the LB distributes traffic to. Servers can be added to or removed from this pool dynamically. A single LB can host many pools (e.g., one pool for /api, another for /static).
2.4Sub-Concept: Health Check#
A health check is a periodic probe the LB sends to each backend server to answer the question "are you alive and ready to serve traffic?" If a server fails its health check, the LB stops sending it traffic (marks it "unhealthy" / "out of rotation"). This is the mechanism that makes server failure invisible to users. Detailed in Section 6.
2.5Sub-Concept: Connection Draining (a.k.a. Graceful Shutdown / Deregistration Delay)#
When you need to take a server out of rotation (to deploy new code, or because it's being scaled down), you don't want to instantly kill it — that would drop the requests currently in flight. Connection draining means: stop sending the server new connections, but allow its existing in-flight requests to complete (up to a timeout, e.g., 30s) before fully removing it. This enables zero-downtime deploys.
2.6Sub-Concept: TLS Termination#
TLS (Transport Layer Security) is the encryption protocol behind HTTPS. TLS termination means the load balancer is the endpoint that decrypts the incoming HTTPS traffic. The client ↔ LB leg is encrypted (HTTPS); the LB ↔ backend leg can then be plain HTTP (inside a trusted private network) or re-encrypted (TLS re-encryption / "TLS passthrough" variants). Terminating TLS at the LB offloads expensive crypto work from your application servers and centralizes certificate management. (More in Section 5.)
That paragraph uses five terms it does not define. Here they are.
What TLS actually does, and why it is expensive. TLS gives three guarantees: confidentiality (an eavesdropper on the wire sees ciphertext), integrity (tampering is detected), and authentication (the client can verify the server really is api.example.com and not an impostor). Establishing those guarantees requires a handshake that runs after the TCP handshake and before any HTTP flows: the client sends a ClientHello listing the cipher suites and TLS versions it supports; the server replies with its choice and its certificate; the two sides perform an asymmetric key exchange (in TLS 1.3, an Elliptic Curve Diffie–Hellman exchange) to agree on a shared secret; and from then on the connection is encrypted with a fast symmetric cipher (typically AES-GCM or ChaCha20-Poly1305) using that secret. The cost is concentrated in the handshake, because asymmetric cryptography is orders of magnitude more expensive per operation than symmetric — a modern core might do a few thousand RSA-2048 handshakes per second but encrypt gigabytes per second symmetrically. That asymmetry is the entire economic argument for terminating TLS at a load balancer: you concentrate the expensive per-connection work in a tier you can scale and accelerate, and you let backends do cheap plaintext.
Round trips matter too. TLS 1.2 costs two extra round trips before the first byte of HTTP; TLS 1.3 cuts it to one, and its 0-RTT resumption mode can send application data in the very first packet on a repeat connection (at the price of replay risk for non-idempotent requests — see File 15). Session resumption (session IDs or session tickets) lets a returning client skip the asymmetric work entirely. This is a load-balancing concern because session tickets must be shared across your LB fleet — if each LB has its own ticket key, a client that lands on a different LB on reconnect suffers a full handshake, and your p99 latency quietly degrades as you add LBs.
A certificate is a file binding a domain name to a public key, signed by a Certificate Authority (CA) the client's operating system or browser already trusts. The client validates the signature chain, checks that the name matches, and checks the expiry. Certificates expire — 90 days for Let's Encrypt, and the industry is shortening this — so automated renewal (ACME) is not a nicety; an expired certificate is a total outage, and it is one of the most common self-inflicted ones in the industry.
SNI (Server Name Indication) is the extension that makes the whole thing work at a load balancer. The problem: the server must present the right certificate, but at handshake time the request's Host: header has not been sent yet — it is inside the encryption that has not been negotiated. SNI solves it by having the client put the hostname **in plaintext in the ClientHello. The LB reads it and selects the matching certificate, which is how one VIP serves hundreds of HTTPS domains. It is also how an L4** load balancer can do a limited kind of hostname routing without decrypting anything: SNI is visible even in a passthrough deployment.
The three deployment modes — this is a question interviewers ask directly:
- TLS termination: the LB decrypts; the LB→backend leg is plain HTTP. Benefit: cheapest, and the LB gets full L7 visibility (routing, WAF, caching, header injection). Cost: traffic is plaintext inside your network, which fails most compliance regimes and any zero-trust model. Mitigation: only acceptable when the LB↔backend network is genuinely trusted and isolated.
- TLS re-encryption (termination + re-origination): the LB decrypts, inspects and routes, then opens a second TLS connection to the backend. Benefit: full L7 features and encryption on the wire everywhere. Cost: you pay for crypto twice, roughly doubling handshake CPU. Mitigation: long-lived keep-alive connections to backends so the backend-side handshake is amortized over many requests.
- TLS passthrough: the LB does not decrypt at all; it forwards the encrypted stream at L4 and the backend terminates. Benefit: true end-to-end encryption, and the LB never holds your private keys. Cost: no L7 anything — no path routing, no header injection, no HTTP retries, no
X-Forwarded-For; you can route only on SNI. Mitigation: Proxy Protocol to give backends the client IP (§11.1). - mTLS (mutual TLS): the ordinary handshake authenticates only the server; in mTLS the client also presents a certificate and the server validates it, so both sides prove identity. This is the backbone of zero-trust service-to-service communication and the main reason to adopt a service mesh (File 10), where a control plane issues and rotates a certificate per workload automatically. Cost: you now operate a private CA and a rotation pipeline — doing this by hand does not scale, which is exactly why the mesh exists.
2.7Sub-Concept: NAT (Network Address Translation)#
NAT is the technique of rewriting the IP addresses (and often ports) inside a packet's headers as it passes through a middlebox, so that the packet appears to come from, or go to, a different machine than it originally did. It exists for two historical reasons, and both matter to load balancing:
- IPv4 address scarcity. There are only ~4.3 billion IPv4 addresses — far fewer than there are devices. The fix: give a whole building/home/company private IP addresses (special reserved ranges like
10.x.x.x,192.168.x.xthat are not routable on the public internet), and put a NAT gateway (your home router, a corporate firewall) at the edge. When any internal device sends a packet out, the gateway rewrites the source IP to its own single public IP (and remembers the mapping in a table so return traffic can be rewritten back and delivered to the right internal device). The consequence that bites load balancing: thousands of distinct users behind one corporate or carrier NAT all appear to the outside world as ONE source IP. This is exactly why IP-hash stickiness (§4.6) breaks — the LB sees one IP and pins an entire company's employees to a single backend server. - Traffic redirection — which is precisely what an L4 load balancer does. There are two directions of rewrite, and the names come up in real configs:
- DNAT (Destination NAT): rewrite the destination address. This is the core L4-LB move: a packet arrives addressed to the VIP (
1.2.3.4:443); the LB rewrites the destination to a chosen backend (10.0.0.12:8080) and forwards it. The client's packet has been silently redirected. - SNAT (Source NAT): rewrite the source address. An LB does this when it wants the backend's reply to come back through the LB — it stamps its own IP as the source, so the backend answers the LB, not the client. The cost: the backend no longer sees the real client IP (the fix for that is
X-Forwarded-Forat L7 or Proxy Protocol at L4 — see §11.1).
- DNAT (Destination NAT): rewrite the destination address. This is the core L4-LB move: a packet arrives addressed to the VIP (
Hold this: **DNAT gets the packet to a backend; SNAT gets the reply back through the LB.** NAT-mode load balancing (§3.2) is built from exactly these two operations, and its performance limits are what motivated DSR.
2.8Sub-Concept: MAC Addresses, L2 Segments, ARP, and the Loopback Interface#
These four terms are prerequisites for understanding Direct Server Return (DSR) in §3.2. They live below IP, at Layer 2 (the data-link layer) of the OSI model (§2.1) — the layer where machines on the same local network actually hand frames to each other. Read §0.5 (encapsulation) and §0.6 (the NIC) first if you have not — this section assumes you know what a frame is and how a NIC filters one.
The short version first, then each piece gets its own full treatment in §2.8.1–§2.8.6 below.
- MAC address. Every network card (NIC) has a MAC address — a burned-in hardware identifier like
aa:bb:cc:dd:ee:01. IP addresses are logical and say "which machine in the world"; MAC addresses are physical and say "which network card on this local network." When a packet travels across the internet its IP addresses stay constant, but at every local-network hop it is wrapped in a frame addressed to the next device's MAC. Delivery on a local network happens by MAC, not by IP — which is the loophole DSR exploits: you can deliver a packet to server B's NIC (by MAC) without changing the IP written inside it. - L2 segment (broadcast domain). An L2 segment is a set of machines connected at Layer 2 — plugged into the same switch/VLAN — that can reach each other directly by MAC address, without going through an IP router. Machines on the same L2 segment are "on the same local network." Machines in different datacenters (or different cloud subnets) are not on the same L2 segment; anything between them must be routed at Layer 3 (IP), and MAC addresses are rewritten at each hop. This is why classic DSR carries the constraint "LB and backends must share an L2 segment": the LB's trick is a MAC-only rewrite, and a MAC-addressed frame cannot cross an IP router.
- ARP (Address Resolution Protocol). ARP is how a machine discovers "what MAC address currently owns IP X on my local segment?" It broadcasts "who has
10.0.0.12?" and the owner replies "me —aa:bb:cc:dd:ee:02." Two ARP facts matter for load balancing: (1) In DSR, backends secretly hold the VIP but must be configured NOT to answer ARP queries for it — otherwise every backend would shout "the VIP is me!" and traffic would bypass the LB chaotically. Only the LB may ARP-respond for the VIP. (2) In LB failover (§11.1), the standby LB that takes over the VIP sends a gratuitous ARP — an unsolicited broadcast announcing "the VIP's MAC is now mine" — so the local router updates its table and traffic shifts to the survivor within seconds. - Loopback interface. Every machine has a virtual, software-only network interface called loopback (
lo, the home of127.0.0.1). Packets addressed to a loopback IP are accepted and processed locally without touching a physical wire. The DSR trick: you can add extra IP addresses to the loopback interface — including the VIP. The backend then accepts packets addressed to the VIP as its own (the kernel says "yes, that IP is me — it's on my loopback") while the loopback's non-ARPing configuration keeps the backend invisible to the segment. That is the entirety of the cryptic phrase "loopback VIP config": **put the VIP onloso the server accepts VIP-addressed packets, and suppress ARP for it so only the LB advertises the VIP.**
Those four bullets are the summary. Now each one properly, because a summary is not an explanation and you cannot debug a DSR deployment from a summary.
2.8.1 The MAC address in full — what it is, where it comes from, what it looks like#
A MAC address (Media Access Control address), also called a hardware address, physical address, or Ethernet address, is a 48-bit (6-byte) identifier belonging to a network interface. It is written as six hexadecimal byte values separated by colons: aa:bb:cc:dd:ee:01. Because it is 48 bits, there are 2⁴⁸ ≈ 281 trillion possible values — enough that every network card ever manufactured can have a globally unique one.
Why it exists at all — the first-principles answer. IP addresses cannot do this job, and understanding why is the point. An IP address is logical and locational: it says "this machine sits in this part of the network topology," and it changes when a machine moves to a different network (your laptop gets a different IP at home than at the office). Routing across the internet requires that locational structure — routers aggregate millions of addresses into a few routing-table entries precisely because addresses are assigned in topological blocks. But at the moment a frame must be physically placed on a wire and delivered to one specific card among the forty plugged into this switch, you need an identifier that is not locational and not reassignable — a name for the hardware itself. That is the MAC address. Layer 3 answers "which network, roughly where in the world"; Layer 2 answers "which physical card, right here."
Structure — the two halves. The 6 bytes split into two 3-byte halves:
- The first three bytes are the OUI (Organizationally Unique Identifier), assigned by the IEEE to the manufacturer.
00:1B:63is Apple,00:50:56is VMware,02:42:ACis what Docker uses for container interfaces. This is whytcpdumpoutput sometimes shows vendor names instead of hex — the tool is looking up the OUI in a table. - The last three bytes are assigned by that manufacturer, uniquely per card. Manufacturer + serial = globally unique.
Two special bits in the very first byte that you should recognize because they explain the special addresses:
- The I/G bit (least significant bit of byte 1):
0means this is a unicast address naming one card;1means multicast — a group address that multiple cards may choose to accept. The all-ones addressff:ff:ff:ff:ff:ffis broadcast, accepted by every card on the segment. ARP depends on broadcast (§2.8.3). - The U/L bit (second-least significant bit of byte 1):
0means the address is universally administered (burned in by the manufacturer),1means locally administered (assigned by software). Cloud virtual NICs, VPN interfaces, and container veth pairs use locally-administered addresses — the hypervisor made them up. This is also why "burned in" is a useful lie rather than a fact:ip link set dev eth0 address 02:11:22:33:44:55changes it at will, which is how MAC spoofing works and why MAC-based security is worthless.
Where you see it. On Linux, ip link show prints each interface's MAC after link/ether. ip neigh show prints the machine's learned mapping of neighbour IPs to MACs (the ARP table, §2.8.3). In a packet capture, tcpdump -e adds the Ethernet header so you can see the source and destination MACs of each frame — without -e, tcpdump hides Layer 2 entirely, which is why so many engineers have never actually looked at one.
The tie-back: the LB's DSR trick is nothing but writing a different value into the 6-byte destination-MAC field of an arriving frame. Everything else in the frame — the entire IP packet, addressed to the VIP — is passed through byte-for-byte unchanged.
2.8.2 Switches, broadcast domains, VLANs, and what "same L2 segment" actually means#
What a switch is and what it actually does. An Ethernet switch is a box with many ports that forwards frames between the machines plugged into it. Its entire intelligence is one data structure: the MAC address table (also called the CAM table, for Content-Addressable Memory, or the forwarding database / FDB), which maps MAC address → the port that address was last seen on. The switch fills this table by itself through backward learning, and the mechanism is worth walking concretely because it explains both DSR and the ARP failure modes.
Take a switch with four ports and three machines, starting with an empty table:
MAC table (empty) Port 1: LB aa:bb:cc:dd:ee:01
+------+------+ Port 2: Server A aa:bb:cc:dd:ee:02
| MAC | Port | Port 3: Server B aa:bb:cc:dd:ee:03
+------+------+ Port 4: router
| | |
+------+------+- The LB sends a frame from
…:01to…:02. The switch receives it on port 1 and does two things. First it learns: "source…:01arrived on port 1," so it writes that row. Second it looks up the destination:…:02is not in the table, so the switch does not know where that machine is, and it floods — sends a copy out every port except the one it arrived on (ports 2, 3, 4). Every machine's NIC receives the frame; Server A's NIC accepts it (destination MAC matches its own), Server B's and the router's NICs silently discard it in hardware (§0.6).
after step 1: | aa:bb:cc:dd:ee:01 | 1 |- Server A replies from
…:02to…:01. The switch learns "source…:02arrived on port 2," and now the destination…:01is in the table → it forwards the frame only out port 1. No flooding. From this point the conversation is private to ports 1 and 2; Server B never sees another byte of it.
after step 2: | aa:bb:cc:dd:ee:01 | 1 |
| aa:bb:cc:dd:ee:02 | 2 |Entries age out after a default of about 300 seconds of silence, so that a machine moved to a different port is eventually re-learned. Crucially, the switch never looks at the IP header. It reads 6 bytes of destination MAC and forwards. That is why switching is done in hardware at line rate while routing costs more — and why a MAC-rewriting load balancer (DSR) is dramatically cheaper per packet than an IP-rewriting one (NAT).
Broadcast domain — the precise definition. A broadcast domain is the set of interfaces that will receive a frame sent to ff:ff:ff:ff:ff:ff. A switch floods broadcasts out every port, so everything connected to one switch — or to a set of switches cabled together — is in one broadcast domain. A router does not forward broadcasts; it is where the broadcast domain stops. "Same L2 segment," "same broadcast domain," and "same local network" all name the same thing: machines that can reach each other by MAC address directly, with no router in between. And because ARP (§2.8.3) works by broadcasting, ARP only ever reaches machines in the same broadcast domain — which is the deep reason DSR carries its topology constraint.
VLAN — subdividing one switch into several segments. A VLAN (Virtual LAN) lets one physical switch behave as several independent switches. Each port is tagged with a VLAN ID (a 12-bit number, so 4094 usable VLANs), and the switch will only forward frames between ports in the same VLAN. Frames crossing between switches on a trunk port carry a 4-byte 802.1Q tag inserted into the Ethernet header, holding that VLAN ID. Two consequences: (1) VLANs let one rack of hardware host several isolated broadcast domains — production, staging, and management traffic sharing switches without seeing each other; (2) traffic between two VLANs must go through a router, so two machines on the same switch but different VLANs are not on the same L2 segment and DSR between them will not work. The 4-byte 802.1Q tag also eats into the MTU budget from §0.5, which is why some VLAN deployments quietly need MTU adjustment.
Why cloud VPCs break all of this. An AWS VPC subnet, a GCP subnet, or an Azure VNet looks like a local network — machines share an address range like 10.0.1.0/24 and reach each other in one hop — but underneath there is no shared Ethernet segment at all. Cloud networks are software-defined overlays: your VM's packets are encapsulated by the hypervisor (AWS uses a scheme built on VXLAN-style encapsulation with its own control plane) and tunnelled across the provider's physical fabric. The provider's SDN controller knows the IP-to-host mapping centrally, so it does not need or honour your ARP broadcasts — it answers them synthetically — and it will not deliver a frame you addressed to some other instance's MAC, because the mapping it enforces is IP-based and anti-spoofing checks drop the rest. This is the concrete, mechanical reason "DSR does not work in most cloud VPCs": not a policy decision, but the absence of the L2 broadcast domain the technique requires. It is also why cloud providers give you managed load balancers (NLB/GLB) that solve the response-path problem differently — with their own encapsulation and distributed data planes.
How to check. ip route shows which destinations your machine believes are directly reachable ("on-link", meaning no gateway listed) versus via a router; anything on-link is in your L2 segment. arping <ip> succeeds only within the broadcast domain. bridge fdb show on a Linux bridge, or show mac address-table on a switch, prints the learned MAC table itself.
The tie-back: DSR rewrites only the destination MAC, and a MAC-addressed frame dies at the first router. Therefore the LB and every backend must sit in one broadcast domain. That single sentence contains the entire constraint.
2.8.3 ARP in full — the request/reply lifecycle, the cache, gratuitous ARP, and the failure modes#
The problem ARP solves. Your machine wants to send a packet to 10.0.0.12, and it has determined from its routing table that 10.0.0.12 is on-link — same segment, no router needed. But to put a frame on the wire it needs a destination MAC address, and it has an IP address. Nothing in the IP address reveals the MAC; they are unrelated namespaces. ARP (Address Resolution Protocol) is the lookup that bridges them, and it is deliberately dumb: it asks everyone.
The full lifecycle, with concrete values. Machine 10.0.0.5 (MAC …:01) wants to reach 10.0.0.12:
- Check the ARP cache first. The kernel keeps a table of recently-resolved IP→MAC mappings. If there's a fresh entry for
10.0.0.12, it is used immediately and no ARP traffic occurs at all. View it withip neigh show(or the legacyarp -n). - Cache miss → broadcast an ARP request. The machine builds a frame with **destination MAC
ff:ff:ff:ff:ff:ff(broadcast)**, source MAC…:01, and **EtherType0x0806** (ARP, not IPv4 — this is not an IP packet at all; ARP rides directly on Ethernet, which is why ARP has no TTL, no routing, and no existence outside the local segment). The ARP payload says, in effect:
ARP REQUEST (opcode 1)
sender MAC : aa:bb:cc:dd:ee:01 sender IP : 10.0.0.5
target MAC : 00:00:00:00:00:00 target IP : 10.0.0.12 <- "who has this?"- Every machine in the broadcast domain receives it (broadcast floods every port, §2.8.2). Each one compares the target IP against its own addresses. Every machine that isn't
10.0.0.12discards the request — but notably, most operating systems opportunistically cache the sender's10.0.0.5 → …:01mapping from the request, on the theory that if someone is asking about you, you will probably be replying to them shortly. - The owner replies — as a unicast, not a broadcast. The machine that holds
10.0.0.12sends an ARP reply (opcode 2) addressed directly back to…:01:
ARP REPLY (opcode 2)
sender MAC : aa:bb:cc:dd:ee:02 sender IP : 10.0.0.12 <- "me, and here's my MAC"
target MAC : aa:bb:cc:dd:ee:01 target IP : 10.0.0.5- The requester caches the mapping and finally sends its real IP packet, wrapped in a frame addressed to
…:02. Total cost: one broadcast and one unicast, once every few minutes per neighbour.
The cache and its states. Linux ARP entries move through a small state machine visible in ip neigh show: REACHABLE (confirmed recently, default ~30 seconds of confidence), STALE (past the confidence window but still usable — the kernel will use it and re-validate in the background), DELAY/PROBE (re-validation in progress), and FAILED (nobody answered). The kernel also gets free confirmation from higher layers: if a TCP connection to that neighbour is successfully exchanging data, the ARP entry is refreshed without any ARP traffic. Entry capacity is bounded by net.ipv4.neigh.default.gc_thresh1/2/3 (default 128/512/1024); on a machine with thousands of neighbours — a busy load balancer on a large flat segment — you will hit gc_thresh3, see neighbour table overflow in dmesg, and suffer intermittent packet loss until you raise it. That is a real, specific load-balancer production failure with a real, specific fix.
Gratuitous ARP — the announcement, not a question. A gratuitous ARP is an ARP message a machine sends unprompted, where the target IP equals its own IP. Its purpose is not to ask anything; it is to tell the entire segment "this IP is now at this MAC — update your caches." It comes in two forms (an unsolicited request for one's own IP, which is the common form because requests are broadcast and therefore reach everyone, or an unsolicited reply), and it has two uses, both directly relevant here:
- VIP failover (§7.2 / §11.1): when a standby LB takes over the VIP from a dead active LB, the VIP has not changed but the MAC that owns it has. Without an announcement, the upstream router would keep sending VIP traffic to the dead box's MAC until its ARP entry aged out — potentially minutes of outage. The gratuitous ARP collapses that to milliseconds: the router sees the broadcast, overwrites its cache entry, and the very next packet goes to the survivor. This is why keepalived's failover is fast, and it is a great interview detail to name.
- Duplicate address detection: on boot, a machine may gratuitously ARP for its own address; if anyone answers, there is an IP conflict.
Failure modes you must be able to name.
- ARP conflict / "the VIP is me!" storm — the DSR misconfiguration. If two or more machines answer ARP for the same IP, the switch's MAC table and every peer's ARP cache flip back and forth between them as each new reply overwrites the last. Traffic for the VIP lands on a random backend, bypassing the LB and its algorithm entirely; connections break mid-flight because consecutive packets of one TCP flow land on different servers. The symptom is intermittent, unreproducible, machine-dependent failure — the worst kind. The diagnosis is one command:
arping -D <VIP>from a neighbouring host, ortcpdump -e -n arpto watch who is answering. The fix is the sysctls in §2.8.5. - Stale cache after a real MAC change — if a NIC is swapped or a VM migrates without a gratuitous ARP, peers keep frames flowing to a MAC that no longer exists until entries age out.
ip neigh flush allis the blunt manual fix. - ARP spoofing / cache poisoning — because ARP has no authentication whatsoever (any machine may claim any IP), an attacker on your segment can simply answer for your gateway's IP and become a man-in-the-middle. This is why flat L2 segments are a security concern and why the isolation of cloud SDN, annoying as it is for DSR, is also a genuine protection.
The tie-back: DSR depends on exactly one machine — the load balancer — answering ARP for the VIP, while the backends silently hold the same address. ARP is the mechanism that must be suppressed for the trick to work, and gratuitous ARP is the mechanism that makes LB failover fast.
2.8.4 The loopback interface in full — and why the VIP goes there#
What it is. The loopback interface (lo on Linux, lo0 on BSD/macOS) is a purely virtual network interface implemented in software by the kernel. It has no cable, no NIC, no MAC address, and no physical existence. Anything transmitted to it is immediately handed back up the receive path of the same machine. Its canonical address is 127.0.0.1 (the whole 127.0.0.0/8 block is reserved for it) plus ::1 in IPv6, and it is always up — it is the one interface that exists even on a machine with no networking at all. Its original purpose is local inter-process communication and testing: your web browser can talk to a server on your own machine at http://127.0.0.1:8080 using the ordinary TCP/IP stack, with no hardware involved.
The three properties that make it the perfect home for a DSR VIP:
- An interface can hold many IP addresses. Nothing in IP requires one address per interface; Linux lets you add as many as you like (
ip addr add 1.2.3.4/32 dev lo). The address is then genuinely owned by the machine. - The Linux weak host model (from §0.6). When a packet arrives, the IP layer accepts it if the destination address is configured on any interface — not only the one it physically arrived on. So a packet addressed to the VIP, arriving over the wire on
eth0, is accepted because the VIP is configured onlo. Without this behaviour DSR would be impossible on Linux; on operating systems with a strict host model, extra configuration is required to get the same effect. - **
lonever puts anything on a wire, and by default nothing ARPs for it.** Sincelohas no physical link, the VIP configured there cannot be advertised by accident through that interface. The remaining risk is that the machine's physical interfaces answer ARP for it on the machine's behalf — which is precisely what the sysctls in §2.8.5 prevent.
**Why a /32 netmask matters.** You add the VIP as 1.2.3.4/32 — a netmask covering exactly one address — rather than, say, /24. A /24 would make the kernel install a route claiming the whole 1.2.3.x network is reachable directly through lo, which would blackhole traffic to every other address in that range: packets destined to 1.2.3.9 would be handed to the local stack instead of out the wire. /32 claims exactly the one address and nothing else. Getting this wrong is a classic self-inflicted outage.
Concrete configuration on a DSR backend (the "loopback VIP config" that the four-bullet summary above waved at):
ip addr add 1.2.3.4/32 dev lo # accept packets addressed to the VIP
sysctl -w net.ipv4.conf.all.arp_ignore=1 # never answer ARP for it (see §2.8.5)
sysctl -w net.ipv4.conf.all.arp_announce=2The tie-back: the loopback VIP is what makes a backend say "yes, that IP is me" to a packet the LB hand-delivered by MAC, without that backend ever telling the network it owns the address.
2.8.5 arp_ignore and arp_announce — every value, explained#
These two Linux sysctls are named constantly in DSR documentation and almost never explained. They are the difference between a working DSR deployment and an ARP war. Both exist per-interface under net.ipv4.conf.<iface>. and globally under net.ipv4.conf.all.; **Linux takes the maximum of the all value and the interface value**, which is why setting all is the safe move.
**arp_ignore — when will this machine answer an ARP request?**
| Value | Behaviour | Meaning for DSR |
|---|---|---|
0 (default) | Reply for any local address, on any interface. | Broken. The backend answers ARP for the VIP on eth0 even though the VIP lives on lo → the "VIP is me!" storm of §2.8.3. |
1 | Reply only if the target IP is configured on the interface the request arrived on. | The DSR setting. A request for the VIP arrives on eth0; the VIP is on lo, not eth0; the machine stays silent. Its own eth0 address still resolves normally, so the machine remains reachable. |
2 | As 1, plus the sender's IP must be within the same subnet as that interface's address. | Stricter variant; also fine for DSR, useful on multi-homed hosts. |
3 | Do not reply for addresses whose route scope is "host" (which includes loopback-scoped addresses). | Also achieves the DSR goal, via scope rather than interface matching. |
4–7 | Reserved. | Unused. |
8 | Never reply to any ARP request at all. | Too blunt — the machine becomes unreachable by IP on its own segment. |
**arp_announce — what source IP will this machine put inside the ARP requests it sends?**
The subtle failure this prevents: when the backend initiates its own outbound traffic and needs to ARP for the gateway, the kernel might, with default settings, use the VIP as the sender IP inside that request — thereby announcing "the VIP is at my MAC" as a side effect of an ordinary lookup. Peers cache it, and you get the same storm through the back door.
| Value | Behaviour | Meaning for DSR |
|---|---|---|
0 (default) | Use any local address as the sender IP, including one from a different interface. | Risky. May leak the VIP as the sender address. |
1 | Avoid addresses not on the target's subnet where possible; prefer a source on the outgoing interface. | Better, but only "where possible" — not a guarantee. |
2 | Always use the best local address on the outgoing interface, ignoring the source of the packet that triggered the lookup. | The DSR setting. The VIP is on lo, never on the outgoing interface, so it can never be announced. |
How to verify it worked: from another machine on the segment, run arping -D <VIP> and confirm exactly one responder — the LB — replies. Or run tcpdump -e -n arp on the segment and watch: you should see requests for the VIP answered only by the LB's MAC. If two MACs answer, you have found your bug.
Persisting it. sysctl -w is lost on reboot; production configuration goes in /etc/sysctl.d/99-dsr.conf. A backend that reboots and comes back with arp_ignore=0 will silently poison the segment — a genuinely nasty incident, because the fleet works fine until the one box that rebooted starts stealing traffic.
The tie-back: these two settings are the entire enforcement mechanism for "only the LB advertises the VIP." Everything else in DSR is just packet plumbing.
2.8.6 Putting it together — one DSR packet, traced through every layer#
Setup: LB at 10.0.0.5 (MAC …:01), backend at 10.0.0.12 (MAC …:02), VIP 1.2.3.4 configured on the LB's eth0 and on the backend's lo, client at 203.0.113.9, all on one L2 segment behind router 10.0.0.1 (MAC …:FF).
- The client sends to
1.2.3.4:443. Routers carry the packet across the internet; the IP header sayssrc 203.0.113.9 → dst 1.2.3.4the whole way, with MACs rewritten at every hop (§0.5). - The last-hop router needs a MAC for
1.2.3.4. It ARPs; only the LB answers (backends are silenced byarp_ignore=1). The router builds a framedst …:01and sends it. - The LB's NIC accepts it (destination MAC matches, §0.6). The LB picks a backend using its algorithm (§4) and rewrites only the 6-byte destination MAC field, from
…:01to…:02. The IP header still reads203.0.113.9 → 1.2.3.4. Nothing else changes. Not the checksum, not the ports, not one byte of payload. - The frame goes back onto the wire. The switch looks up
…:02in its MAC table (§2.8.2) and forwards it out the backend's port. - The backend's NIC accepts the frame (destination MAC matches). The kernel strips Ethernet, sees destination IP
1.2.3.4, and asks "do I own this?" — **yes, it is onlo** (§2.8.4, weak host model). The packet is accepted, handed to TCP, delivered to the socket listening on port 443. - The application responds. The kernel builds a reply with **source IP
1.2.3.4** (the socket is bound to the VIP) and destination203.0.113.9, routes it to the default gateway10.0.0.1— not through the LB — wrapped in a frame addressed to the router's MAC…:FF. - The client receives a response whose source is
1.2.3.4, exactly the address it sent to. Its TCP stack matches it to the existing connection and accepts it. The client has no way to detect that the response came from a different machine than the one the request was steered to.
Total load-balancer involvement: it touched 6 bytes, on the inbound direction only, and never saw the response at all. That is the whole economic argument for DSR.
2.9Sub-Concept: Deployment Strategies — Rolling, Blue-Green, Canary#
Load balancers are the instrument of safe deployments, and these three terms appear throughout this file (and in every system design interview), so define them properly:
- Rolling deploy. Update the fleet a few servers at a time: drain server 1 (connection draining, §2.5), deploy new code to it, health-check it, re-add it to the pool; repeat for server 2, 3, … N. The service never goes down because most of the fleet is always serving. Benefit: zero extra hardware. Cost: during the roll, two versions run simultaneously — your code and schema changes must be backward-compatible for that window — and a bad version is discovered only after it has spread to some real users. Mitigation: health checks gate each step; halt/roll back on failures.
- Blue-green deploy. Run two complete environments: "blue" (current version, taking all traffic) and "green" (new version, idle). Deploy to green, test it privately, then flip the LB/DNS to point all traffic at green in one atomic switch. Rollback is the reverse flip — seconds, not a re-deploy. Benefit: instant, clean rollback; no mixed-version window for traffic. Cost: you pay for 2× the infrastructure during the transition, and in-flight state (open WebSockets, local caches) doesn't migrate across the flip. Mitigation: keep servers stateless (§6.3) so a flip strands nothing.
- Canary deploy. Named after the canary in a coal mine — a small, sacrificial early-warning signal. Deploy the new version to a tiny slice of the fleet and use the LB's weighted routing (§4.2) to send it a small fraction of real traffic — say 1%. Watch error rates, latency, and business metrics on the canary versus the baseline. Healthy? Ramp: 1% → 5% → 25% → 100%. Unhealthy? Pull the canary; only 1% of users ever saw the bug. Benefit: real-traffic validation with a strictly bounded blast radius. Cost: needs good per-version observability (you must be able to compare canary metrics against baseline), slower full rollout, and again a mixed-version window. This is why weighted round robin (§4.2) lists "canary" as a use case — a weight is exactly the knob that says "give this one server 1% of traffic."
Decision rule: rolling is the cheap default; blue-green when you need instant rollback and can afford 2× capacity for an hour; canary when the change is risky enough that you want real-traffic proof before betting the fleet on it. Serious shops combine them: canary first, then roll.
With these sub-concepts defined, we can now go under the hood.
3Deep-Dive Mechanics#
3.1The Life of a Request Through a Load Balancer#
Let's trace an HTTPS request end-to-end through a Layer 7 load balancer. This is the exact flow you should be able to draw on a whiteboard.
CLIENT LOAD BALANCER (VIP: 1.2.3.4:443) BACKEND POOL
(browser) (10.0.0.11..13:8080)
| |
| 1. DNS lookup: api.example.com -> 1.2.3.4 |
|----------------------------------------------------------------------|
| 2. TCP handshake (SYN, SYN-ACK, ACK) to 1.2.3.4:443 |
|------------------------------------------->| |
| 3. TLS handshake; LB presents certificate | |
|<------------------------------------------>| (TLS terminated here) |
| 4. HTTP request: GET /api/users HTTP/1.1 | |
| Host: api.example.com | |
|------------------------------------------->| |
| | 5. LB reads L7 data: |
| | path=/api/users |
| | picks pool + algorithm|
| | picks server 10.0.0.12|
| | 6. LB opens/reuses conn |
| |---------------------->| |
| | (plain HTTP or mTLS) | 7. app processes
| |<----------------------| | returns 200
| 8. LB relays response back to client | |
|<-------------------------------------------| |Key mechanical points:
- The client only ever completes a TCP + TLS handshake with the VIP. It has no idea server
10.0.0.12exists. - The LB maintains two separate TCP connections: one to the client, one to the backend. This is called a full proxy architecture. The LB is a man-in-the-middle by design.
- Because it's a full proxy at L7, the LB can do smart things: retry a failed backend on a different server, buffer slow clients, rewrite headers, add
X-Forwarded-For(so the backend knows the real client IP), compress responses, etc.
3.2The Four Forwarding Architectures: Full Proxy, NAT, DSR, and IP Tunneling#
This is a classic senior-level distinction. There are four fundamentally different ways an LB can move a packet to a backend, and they differ in exactly one dimension that matters enormously at scale: what does the LB rewrite, and which direction(s) of traffic must flow through it? (These map directly to the modes of LVS — Linux Virtual Server, the kernel L4 balancer, which calls them NAT mode, DR mode, and TUN mode; full proxy is what Nginx/HAProxy/ALB do.)
(A) Full Proxy Mode (the default, e.g., Nginx, HAProxy, AWS ALB): Both request and response flow through the LB. The LB terminates the client's TCP connection entirely and originates a brand-new connection to the backend — two independent conversations glued together by the LB. Response traffic returns via the LB.
- Pro: Full L7 visibility and control (rewrite, retry, buffer, TLS termination). Because the LB owns both connections, it can even speak different protocols on each side (HTTP/2 to the client, HTTP/1.1 to the backend).
- Con: The LB must handle both directions of traffic, including large response bodies (video, downloads). Response bandwidth is often 10–100× request bandwidth, so the LB becomes a bandwidth bottleneck. It also pays the CPU cost of terminating and re-originating every connection.
(B) NAT Mode (L4 — the "rewrite the addresses" technique): The LB does not terminate the connection; it forwards the client's own packets, rewriting their headers using the NAT operations defined in §2.7. This is the classic LVS-NAT mode and the mental model behind "an L4 LB uses NAT" (§5.1).
- Mechanism, step by step: (1) A packet arrives addressed to
VIP:443. (2) The LB picks a backend via its algorithm, records the flow in its conntrack table (§3.3), and performs DNAT: destination is rewritten to10.0.0.12:8080. The source (the client's real IP) is left intact. (3) The backend processes the request and replies — but here is the subtlety: the reply must pass back through the LB, because the client is expecting a response from the VIP, and a reply coming straight from10.0.0.12would be rejected (the client has no connection with that address). Two ways to guarantee this: make the LB the backends' default gateway (all their outbound traffic routes through it — classic LVS-NAT), or have the LB also do SNAT on the way in ("full-NAT": source rewritten to the LB's IP, so backends naturally reply to the LB — at the cost of hiding the real client IP, mitigated with Proxy Protocol, §11.1). (4) The LB rewrites the reply's source from10.0.0.12back to the VIP and sends it to the client. The client never knows. - Pro: Simple, works with unmodified backends (no special loopback config), backends can be on any subnet in full-NAT, keeps L4 speed (no TLS/HTTP parsing, no connection termination).
- Con: Both directions still traverse the LB (same bandwidth-bottleneck shape as full proxy, just cheaper per packet), and in classic gateway-mode NAT all backends must route through the LB, which caps fleet size. This response-path bottleneck is precisely the pain that motivated DSR.
(C) Direct Server Return (DSR) / Direct Routing (L4 technique): The request goes client → LB → backend, but the response goes backend → client directly, bypassing the LB entirely. The LB only sees inbound traffic. (Uses the Layer-2 machinery from §2.8 — read that first.)
- Mechanism: The LB rewrites only the destination MAC address of the incoming frame to the chosen backend's MAC — the IP headers are untouched, still
client → VIP. The frame is delivered by MAC (§2.8) to the backend's NIC. The backend accepts it because the VIP is configured on its loopback interface ("that IP is me"), while its ARP responses for the VIP are suppressed so only the LB advertises the VIP on the segment. The backend processes the request and replies directly to the client, with the VIP as the source IP — which the client happily accepts, because from its point of view the reply came from exactly the address it was talking to. - Pro: The LB is freed from handling the massive response volume — it only handles the small inbound requests. Enables extremely high throughput per LB box (used by CDNs, video streaming, and large L4 tiers).
- Con — unpacked: (1) L4 only. The LB never sees responses and never terminates the connection, so no HTTP awareness, no TLS termination, no retries, no response inspection or modification — ever. (2) Same-L2-segment requirement: the trick is a MAC rewrite, and MAC-addressed frames cannot cross an IP router (§2.8), so LB and backends must sit in the same broadcast domain — which is impossible in most cloud VPCs and constrains rack/switch topology on-prem. (3) Loopback VIP config on every backend: each server needs the VIP added to
loplus ARP suppression (arp_ignore/arp_announcesysctls on Linux); getting this wrong on one box causes ARP fights over the VIP and intermittent, maddening traffic misrouting. (4) Port rewriting is impossible (the IP/TCP headers aren't touched), so backends must listen on the VIP's port.
(D) IP Tunneling Mode (LVS-TUN — "DSR across routers"): Same goal as DSR (responses bypass the LB) but without the same-L2-segment constraint.
- Mechanism: Instead of a MAC rewrite, the LB encapsulates the original packet inside a new IP packet (IP-in-IP: a new outer IP header addressed to the backend's real IP, with the untouched original packet as payload) and routes it — across any number of routers, even to another datacenter. The backend decapsulates (unwraps the outer header), finds the original
client → VIPpacket inside, accepts it (VIP on loopback again), and replies directly to the client as the VIP. - Pro: DSR's response-bypass economics plus backends anywhere IP can reach — the basis of some global anycast designs.
- Con: Backends must support the tunnel protocol and decapsulation; ~20 bytes of encapsulation overhead per packet can force fragmentation/MTU tuning; still L4-only with all of DSR's blindness.
Comparison — the one table to remember:
| Mode | LB rewrites | Response path | L7 features? | Backend constraint | Bottleneck |
|---|---|---|---|---|---|
| Full proxy | Nothing (new connection) | Through LB | ✅ Full | None | LB bandwidth + CPU |
| NAT | IP headers (DNAT ± SNAT) | Through LB | ❌ | Route replies via LB | LB bandwidth |
| DSR | MAC only | Direct to client | ❌ | Same L2 segment + loopback VIP | Inbound only (tiny) |
| IP Tunnel | Encapsulates (IP-in-IP) | Direct to client | ❌ | Tunnel support + loopback VIP | Inbound only (tiny) |
Decision rule: full proxy for anything needing HTTP intelligence (the web-app default); NAT mode for simple L4 balancing where response volume is modest; DSR/tunneling when responses dwarf requests (video, downloads, CDN edges) and you can pay the network-config complexity.
3.3How the LB Maintains State#
A Layer 4 LB maintains a connection tracking table (conntrack). For every active TCP flow it stores a 5-tuple:
(source IP, source port, dest IP, dest port, protocol) -> chosen backend serverThis ensures every packet of the same TCP connection goes to the same backend (you cannot send packet 1 of a TCP stream to server A and packet 2 to server B — TCP state would break). This table is why L4 LBs must be stateful per-connection, and why LB failover is tricky (Section 11).
A Layer 7 LB additionally parses the HTTP stream, so it can make a new routing decision per HTTP request, even multiplexing many requests from one client connection across different backends (especially with HTTP/2 and keep-alive).
4Routing Algorithms — Exhaustive Breakdown#
The "policy" for choosing which backend gets the next request is the routing (or balancing) algorithm. Interviewers love asking you to compare these. Here is every one that matters.
4.1Round Robin#
Mechanism: Maintain a pointer. Server 1, then 2, then 3, ..., then N, then back to 1. Purely sequential rotation.
- Pros: Dead simple, zero state about server load, perfectly even count distribution.
- Cons: Assumes all servers are equally powerful and all requests are equally expensive. Both assumptions are often false. A request that triggers a 10-second report generation counts the same as a 2ms health ping. Uneven request cost → uneven actual load despite even count.
- When to use: Homogeneous fleet, uniform request cost, stateless requests. A sane default.
4.2Weighted Round Robin#
Mechanism: Assign each server a weight (e.g., a beefy 32-core box gets weight 4, an old 8-core box gets weight 1). The box with weight 4 receives 4× the requests per cycle.
- Pros: Handles heterogeneous hardware (mixed instance types).
- Cons: Weights are static and manually tuned; they don't adapt to real-time load. A weighted-heavy server having a bad moment still gets hammered.
- When to use: Fleets with mixed instance sizes (a pool that grew over years contains several hardware generations — weight each proportionally to its capacity so the old boxes aren't crushed), or canary deploys (§2.9) — the weight is literally the canary knob: give the one server running the new version weight such that it receives ~1% of traffic, watch its metrics, and ramp the weight up as confidence grows.
4.3Least Connections#
Mechanism: Route the new request to the server with the fewest active connections right now.
- Pros: Adapts to reality. If one server is stuck with many slow long-lived connections, it stops receiving new ones. Far better than round robin for long-lived or variable-duration connections (WebSockets, DB connections, streaming).
- Cons: Requires the LB to track live connection counts (more state). "Fewest connections" ≠ "least loaded" if connections vary wildly in CPU cost.
- When to use: WebSockets, long-lived connections, requests with highly variable duration.
4.4Weighted Least Connections#
Combines the above two: normalize active connections by server weight/capacity. Route to whoever has the lowest active_connections / weight ratio. Best general-purpose adaptive algorithm for mixed fleets.
4.5Least Response Time (Least Latency)#
Mechanism: Route to the server with the lowest combination of active connections and lowest measured recent response time (EWMA — exponentially weighted moving average of latency).
- Pros: Most "intelligent" — directly optimizes what users feel (latency). Automatically routes away from a server that's degrading (GC pause, disk contention).
- Cons: Most state and measurement overhead. Can create feedback loops / herd behavior (everyone piles onto the currently-fastest server, making it slow). Needs damping.
- When to use: Latency-critical services where backends can degrade independently.
4.6IP Hash / Source Hash#
Mechanism: hash(client_source_IP) mod N → picks the server. The same client IP always maps to the same server (as long as N doesn't change).
- Pros: Provides session stickiness without cookies — useful when the backend holds per-user in-memory state.
- Cons: Uneven distribution if client IPs cluster — the classic case is NAT (§2.7): a corporate or carrier NAT gateway rewrites every internal user's source address to its single public IP, so 10,000 employees arrive at your LB as one IP, hash to one server, and melt it while its siblings idle. And
mod Nreshuffles everything when N changes (this is the exact problem Consistent Hashing, File 06, solves — foreshadowing!). - When to use: Legacy stateful backends needing stickiness; a stopgap, not a design goal.
4.7Consistent Hashing (preview)#
Mechanism: Instead of hash mod N, map both servers and requests onto a hash ring, and route each request to the next server clockwise on the ring. Adding/removing a server only remaps 1/N of keys instead of nearly all of them.
- This deserves — and gets — its own entire file (File 06). For load balancing purposes, know that it's the "sticky routing that survives fleet resizing" algorithm, heavily used for cache-server routing and sharded backends.
4.8Random (with Two Choices — "Power of Two Choices")#
Mechanism: Pick two servers at random, then send the request to whichever of the two has fewer connections.
- Pros: Astonishingly effective. Pure random is bad, but "pick 2, choose the lighter" gives near-optimal load balancing with almost no coordination or global state. Backed by strong probabilistic theory ("the power of two random choices" dramatically reduces maximum load).
- Cons: Slightly worse than perfect least-connections, but far cheaper to compute at scale.
- When to use: Massive-scale distributed LBs where maintaining global least-connection state is too expensive. Netflix, Google, and many service meshes use variants of this.
4.9Summary Table#
| Algorithm | Adapts to load? | Needs state? | Sticky? | Best for |
|---|---|---|---|---|
| Round Robin | No | No | No | Uniform, stateless fleet |
| Weighted RR | No (static) | No | No | Mixed hardware |
| Least Connections | Yes | Yes | No | Long-lived / variable requests |
| Weighted Least Conn | Yes | Yes | No | Mixed hardware + variable load |
| Least Response Time | Yes | Yes (latency) | No | Latency-critical services |
| IP / Source Hash | No | Minimal | Yes | Legacy sticky backends |
| Consistent Hash | No | Ring | Yes | Cache routing, sharded backends |
| Power of Two Choices | Yes | Minimal | No | Hyperscale, service mesh |
5Layer 4 vs. Layer 7 — The Core Distinction#
This is the single most common load-balancing interview question. Master it cold.
5.1Layer 4 Load Balancer#
Operates on TCP/UDP. Makes routing decisions using only IP + port. Does not decrypt TLS or read HTTP.
How it works: Uses one of the L4 forwarding modes from §3.2 — NAT (rewrite the packet's destination/source IPs, §2.7), DSR (rewrite only the MAC, responses bypass the LB), or IP tunneling (encapsulate and forward). In every case it merely rewrites headers and forwards the client's own packets — it never terminates the connection or parses the payload. Because it does almost no work per packet, it is blazingly fast and handles enormous throughput with tiny latency (microseconds).
Examples: AWS Network Load Balancer (NLB), Google TCP/UDP LB, LVS (Linux Virtual Server), F5 in L4 mode.
Characteristics:
- ✅ Extremely high throughput, ultra-low latency (near line-rate).
- ✅ Protocol-agnostic — balances anything over TCP/UDP (databases, game servers, MQTT, custom binary protocols).
- ✅ Preserves the client connection semantics; can preserve source IP.
- ❌ Blind to content. Cannot route by URL path, cannot do cookie stickiness, cannot inspect headers.
- ❌ Cannot terminate TLS (in pure L4) — no HTTPS-level features.
- ❌ A retry means retrying the whole TCP connection, not an individual request.
5.2Layer 7 Load Balancer#
Operates on HTTP/HTTPS/gRPC. Terminates the connection, reads the full application message, and routes on content.
Examples: AWS Application Load Balancer (ALB), Nginx, HAProxy (in HTTP mode), Envoy, Traefik, Google HTTPS LB.
Characteristics:
- ✅ Content-based routing:
/api/*→ API pool,/static/*→ static pool,/video/*→ media pool. Host-based routing:shop.example.comvsblog.example.comon the same VIP. - ✅ TLS termination and centralized certificate management.
- ✅ Cookie-based session stickiness, header manipulation,
X-Forwarded-Forinjection. - ✅ Per-request routing, retries, and observability (per-URL metrics, rate limiting, and WAF integration — a Web Application Firewall, an L7 filter that inspects each HTTP request against attack-signature rules (SQL injection, bad bots) and drops matches before they reach your servers; it can only exist at L7 because it needs to read the request's content).
- ✅ HTTP/2 and gRPC multiplexing, request buffering, compression.
- ❌ Higher latency and CPU cost (it parses every request, does crypto).
- ❌ Lower raw throughput than L4.
- ❌ HTTP-family protocols only.
5.3The Decision Rule#
Use Layer 7 when you need to make decisions based on what the request is (URLs, headers, cookies) or need HTTP features (TLS termination, path routing, WAF). This is the default for web applications and APIs.
Use Layer 4 when you need raw speed and protocol independence, when you're balancing non-HTTP traffic (databases, custom protocols, gaming), or when you want to preserve the client's source IP and do minimal processing.
Real production pattern: They are frequently stacked. An L4 NLB at the very edge (absorbs huge connection volume, handles DDoS, static IPs), forwarding to a fleet of L7 proxies (Envoy/ALB) that do the smart HTTP routing. Best of both worlds.
6Health Checks, Failover & Session Persistence#
interval × threshold seconds of user-visible errors before eviction. That product is your real
detection time — compute it out loud in an interview.6.1Health Checks — Deep Dive#
A health check answers "should this server receive traffic?" There are two philosophies:
(A) Active health checks (LB → server): The LB proactively pings each backend on a schedule.
- L4 active check: Attempt a TCP connection to the port. If the handshake completes, "healthy." Cheap but shallow — the port can be open while the app is deadlocked.
- L7 active check: Send
GET /healthzand require200 OK(optionally matching a response body). This is far more meaningful because a good/healthzendpoint checks the app's real readiness: "can I reach the database? are my caches warm? is my thread pool alive?"
Tunable parameters (know these terms):
- Interval: how often to probe (e.g., every 5s).
- Timeout: how long to wait for a reply before counting it a failure (e.g., 2s).
- Unhealthy threshold: how many consecutive failures before marking dead (e.g., 3). Prevents flapping on a single blip.
- Healthy threshold: how many consecutive successes before returning to rotation.
(B) Passive health checks (observe real traffic): Instead of (or in addition to) probing, the LB watches real request outcomes. If a server returns a burst of 5xx errors or connection resets, eject it. Envoy calls this outlier detection. More responsive than active checks (catches problems between probe intervals) but reactive (some real users hit the error first).
6.2Liveness vs. Readiness (crucial distinction)#
- Liveness: "Is the process alive?" If it fails, restart the container.
- Readiness: "Is the process ready to serve traffic right now?" If it fails, stop routing to it but don't kill it — it may just be warming up caches or draining. Load balancers care about readiness. Confusing the two is a classic bug: marking a slow-warming server as dead and killing it in a restart loop ("crash loop").
6.3Session Persistence (Stickiness) — Deep Dive#
The problem: some backends store per-user state in memory (e.g., a shopping cart, a WebSocket session, a login session cached locally). If request 1 lands on server A (which now holds your cart) but request 2 lands on server B (which knows nothing), the app breaks.
Solutions, worst to best:
- Source IP hash stickiness — same client IP → same server. Breaks behind NAT/proxies (see 4.6).
- Cookie-based stickiness — the LB injects a cookie (e.g.,
AWSALB) identifying the chosen backend; subsequent requests with that cookie route back to the same server. More reliable than IP hash. - The real fix — make servers stateless. Externalize session state to a shared store (Redis, Memcached, a database). Then any server can serve any request, and you don't need stickiness at all. This is the architecturally correct answer and the one interviewers want to hear. Stickiness is a crutch; statelessness is the cure. (This directly motivates File 02, Caching.)
7Pros, Cons & Trade-offs#
7.1Benefits (Why you always have one)#
- High availability: dead servers are bypassed automatically.
- Horizontal scalability: the enabler of "just add servers."
- Zero-downtime deploys: drain, deploy, re-add (rolling deploys, blue-green, canary).
- Operational flexibility: patch/reboot servers one at a time behind the LB.
- Security chokepoint: central place for TLS termination, WAF, DDoS mitigation, rate limiting.
- Observability: a single place to measure RPS, latency, error rates per route.
7.2Costs & Trade-offs (What interviewers probe)#
- It can become the SPOF itself. If you have one LB and it dies, everything is down. Mitigation: run LBs in a highly-available pair/cluster (active-active or active-passive) with a floating VIP. The standard mechanism is VRRP (Virtual Router Redundancy Protocol), most commonly deployed via the Linux daemon keepalived: two LB boxes share the VIP, the active one holds it and sends heartbeat advertisements every ~1s; if the standby misses ~3 heartbeats it concludes the active is dead, claims the VIP, and broadcasts a gratuitous ARP (§2.8) so the upstream router immediately re-learns which MAC owns the VIP — failover completes in seconds with no DNS change. Alternatively, use a managed cloud LB that's internally redundant across AZs. Never draw a single LB box in a serious design without noting its redundancy.
- Added latency: every request now takes an extra network hop and, at L7, parsing + crypto. Usually sub-millisecond in-region, but it's non-zero.
- Bandwidth bottleneck (full proxy): response traffic flows through it. Large downloads/video can saturate the LB's NIC. Mitigation: DSR, or offload static/video to a CDN (File 05).
- Operational complexity: health check tuning, TLS cert rotation, sticky-session edge cases, connection draining timeouts. A misconfigured health check can take down a healthy fleet (mark everything unhealthy → no servers in rotation → total outage).
- Cost: managed LBs bill per hour + per GB + per new connection (LCU/NLCU units on AWS). At scale this is a real line item.
8Interview Diagnostic Framework (Symptom → Solution)#
Use this "if you see this, reach for load balancing (and which flavor)" map.
| System Symptom (what you're told in the interview) | Why LB is the cure | Specific solution |
|---|---|---|
| "One server can't handle the traffic; it's maxed out." | Need horizontal scale; something must distribute across many servers. | Put an LB in front; scale out the pool. |
| "When a server crashes, users get errors." | Failure isn't being routed around. | LB with health checks + automatic failover. |
| "We can't deploy without downtime." | No way to rotate servers out gracefully. | LB + connection draining + rolling/blue-green deploy. |
| "Traffic isn't even — some servers idle, some melt." | Poor distribution policy. | Switch algorithm: least-connections / least-response-time; check for sticky-session skew. |
| "We have mixed instance sizes and small boxes fall over." | Equal distribution ignores capacity. | Weighted round robin / weighted least connections. |
"We need /api and /static handled by different fleets." | Requires content awareness. | Layer 7 LB with path-based routing. |
| "We're balancing a database / game server / MQTT, not HTTP." | Non-HTTP protocol. | Layer 4 LB (NLB/LVS). |
| "Our video responses are saturating the balancer." | Full-proxy bandwidth bottleneck. | DSR (L4) and/or offload to CDN. |
| "Corporate users all land on one server." | Source-IP-hash skew behind NAT. | Move to cookie stickiness, or better, stateless servers + shared session store. |
| "Users in another continent see huge latency." | Single-region LB. | Global/geo load balancing + multi-region (leads into CDN/anycast, File 05). |
9Real Implementations & Product Landscape#
Everything so far has been mechanism. This section is the catalogue: the actual software and services that implement these mechanisms, what each one is, what it is genuinely good at, where it falls down, and — the part that matters in an interview and on the job — which one you should pick in which situation. When this file (or an interviewer) says "use NGINX" or "put an ALB in front," this is the section that makes that sentence mean something. A name without its landscape teaches nothing: ALB and NLB are both "AWS load balancers" and they are not remotely interchangeable.
The landscape divides into six families, ordered roughly from lowest layer to highest.
9.1Family 1 — Kernel-space and hardware-accelerated L4 balancers#
These live inside the operating system kernel or on the NIC itself. They do not terminate connections, do not understand HTTP, and are optimized for one thing: moving enormous packet volumes per box with minimal CPU. This is the family that uses the NAT / DSR / tunnel modes of §3.2 directly.
LVS / IPVS (Linux Virtual Server). LVS is the original open-source L4 load balancer, built into the Linux kernel since 2.4 as the IPVS module, created by Wensong Zhang in 1998. It is not a daemon that receives and re-sends traffic; it is a hook inside the kernel's packet-processing path, which is why it is fast — a packet is switched to a backend without ever being copied to user space or touched by a scheduler. It is the canonical implementation of the three forwarding modes in §3.2 (NAT, DR, TUN) — those mode names come from LVS — and it implements about ten scheduling algorithms including round robin, weighted round robin, least connection, weighted least connection, and source hashing (§4). You configure it with ipvsadm, and in practice you never run it alone: it is paired with keepalived, which adds health checking of the backends and VRRP-based failover of the VIP between two LVS boxes (§7.2). Best at: extremely high-throughput L4 balancing on hardware you own, especially with DR mode where a single mid-range box can front hundreds of gigabits of egress. Limits: no L7 whatsoever — no TLS termination, no path routing, no header manipulation, no retries; configuration is imperative and awkward to automate; DR mode drags in the whole L2 constraint of §2.8. Cost: free, but you are operating Linux boxes and a VRRP pair yourself. Reach for it when: you run on-prem or bare metal, response volume dwarfs request volume, and you need a cheap L4 tier under your L7 tier. Not when: you are in a cloud VPC (DR mode won't work, §2.8.2) or you need anything HTTP-aware.
Google Maglev. Maglev is Google's software network load balancer, described in their 2016 NSDI paper, and it is the design that changed how the industry thinks about L4 tiers. Instead of a special appliance, Maglev runs as a user-space program on ordinary commodity servers, bypassing the kernel networking stack entirely (it takes packets directly from the NIC via a kernel bypass and a shared memory ring). Many Maglev machines simultaneously announce the same VIP to the network via BGP, and the routers spread incoming flows across them using ECMP (Equal-Cost Multi-Path) — hardware-level load balancing of the load balancers. Two design details are famous. First, it forwards using GRE encapsulation and DSR-style return, so responses bypass Maglev entirely. Second, and more importantly, it uses a purpose-built consistent hashing scheme (Maglev hashing) that gives near-perfect load distribution and ensures that different Maglev machines, having never spoken to each other, independently choose the same backend for the same flow — which is what makes it safe for a router to reshuffle a flow to a different Maglev box mid-connection. Best at: planetary-scale L4 with no single-machine ceiling; adding capacity is adding a machine. Limits: it is Google-internal; you consume it as GCP's Network Load Balancer, not as software you install. The interview value: naming Maglev and explaining ECMP + consistent hashing is the difference between "run two load balancers" and "here is how you scale the load-balancing tier itself."
Facebook Katran and Cloudflare Unimog are the modern open/published equivalents, both built on XDP (eXpress Data Path) and eBPF — a Linux facility that lets you attach a small verified program to the NIC driver's receive path so packets are inspected and redirected before the kernel allocates its usual per-packet data structures. This gets you Maglev-class performance (tens of millions of packets per second per box) on stock Linux. Katran is open source and usable; both use consistent hashing and encapsulation for the same reasons Maglev does. Reach for these when you are building your own L4 tier at very large scale and cannot use a cloud provider's.
Hardware appliances — F5 BIG-IP, Citrix ADC (formerly NetScaler), A10. These are dedicated physical boxes with purpose-built ASICs, sold with support contracts, and they still run a large fraction of enterprise and financial-sector traffic. They do L4 and full L7, plus a long tail of enterprise features (WAF, SSL VPN, sophisticated iRules scripting). Best at: regulated environments needing a vendor to call, hardware crypto acceleration, and feature depth accumulated over twenty years. Limits: very expensive (list prices routinely in the tens of thousands per appliance plus annual support), long procurement cycles, capacity bought in fixed increments, and a configuration model that resists modern GitOps automation. Reach for them when you are in an enterprise that already owns them or has compliance requirements naming them. Not when you are building anything cloud-native — the economics and the operational model both fight you.
9.2Family 2 — User-space software proxies (the L7 workhorses)#
These are full proxies (§3.2 A): they terminate the client's connection, parse HTTP, and open their own connection to a backend. This is the family you will actually configure in most jobs.
NGINX. Written by Igor Sysoev around 2004 to solve the C10K problem (serving ten thousand simultaneous connections on one machine, which the then-standard thread-per-connection model could not do), NGINX's defining architectural choice is an event-driven, asynchronous, single-threaded-per-worker model: a small fixed number of worker processes, each running an event loop over thousands of connections, rather than a thread or process per connection. That is why its memory footprint per connection is a few kilobytes instead of a megabyte-scale thread stack, and why it handles slow clients gracefully. It is simultaneously a web server, a reverse proxy, a load balancer, and a cache — which is its great practical advantage: one binary does static file serving, TLS termination, and balancing. It supports round robin, weighted round robin, least connections, IP hash, and generic hash including consistent hashing (§4), plus passive health checks. Limits worth knowing: in the free open-source version, active health checks (§6.1) and dynamic reconfiguration of the upstream pool via an API are NGINX Plus (commercial) features; open-source NGINX picks up backend changes by reloading its configuration file, which is graceful but is still a config-file-driven workflow rather than an API-driven one. Reach for it when: you want one well-understood, extremely stable component that terminates TLS, serves static assets, caches, and balances — the default choice for a single-service or small-fleet deployment. Not when: you need per-request dynamic backend discovery in a rapidly changing service mesh (Envoy fits better) or deep TCP-level observability (HAProxy fits better).
HAProxy. Written by Willy Tarreau starting in 2000, HAProxy is the specialist: it is only a proxy and load balancer, and that focus shows. It has the richest health-checking system of the open-source options (including expect-based checks that assert on response content, agent checks where the backend reports its own weight, and slow-start ramping for newly-added servers), the best-in-class runtime statistics page and CSV/Prometheus metrics, a runtime admin socket that lets you change weights, drain servers, and add backends without a reload, and unusually precise timeout controls. It operates in both TCP mode (L4) and HTTP mode (L7). Best at: operational control and observability; if you need to know exactly why a request was slow, HAProxy's logs tell you the queue time, the connect time, the server response time, and the total time as separate fields. Limits: it does not serve static files or act as a general web server, so you usually run it alongside something that does; its configuration language, while powerful, is dense. Reach for it when: load balancing is the whole job and you care about health-checking sophistication, draining, and metrics — a high-traffic API tier, a database read-replica pool, a TCP service. Not when: you also wanted a web server and a cache in the same process.
Envoy. Created at Lyft in 2016 and now a CNCF project, Envoy was designed for a world NGINX and HAProxy predate: thousands of ephemeral microservice instances that appear and disappear continuously. Its defining choice is the xDS APIs — a set of gRPC/REST discovery services (LDS for listeners, RDS for routes, CDS for clusters, EDS for endpoints) through which a control plane pushes configuration to Envoy dynamically, at runtime, with no reload at all. Configuration is data streamed from a controller, not a file on disk. On top of that it has native HTTP/2 and gRPC support (including gRPC-specific load balancing, which matters because gRPC multiplexes many requests over one long-lived connection and therefore defeats connection-level balancing), first-class distributed tracing, per-upstream circuit breaking and outlier detection (automatic ejection of misbehaving hosts), retries with budgets, and the panic threshold behaviour described in §11.2. It is the data plane inside Istio, Consul Connect, and AWS App Mesh, and it is what AWS's own ALB-adjacent tooling and many CDNs build on. Limits: it is heavy — meaningfully more memory and CPU per instance than NGINX — and its configuration is verbose YAML/protobuf that is genuinely painful to hand-write; it presumes a control plane, and if you don't have one you are doing the hardest part yourself. Reach for it when: you are running a service mesh (File 10), you need gRPC-aware balancing, or your backend set changes constantly. Not when: you have three static backends and a config file would have taken five minutes.
Traefik. A Go proxy designed for container platforms, whose distinguishing feature is automatic configuration by service discovery: it watches the Docker, Kubernetes, or Consul API and materializes routes from labels/annotations on your services, so deploying a container with the right label makes it routable with no configuration file edited anywhere. It also automates TLS certificates via ACME/Let's Encrypt out of the box. Best at: developer ergonomics on container platforms — the fastest path from "container running" to "HTTPS URL working." Limits: less battle-tested at extreme scale than NGINX/HAProxy, and historically less predictable performance under heavy load. Reach for it when: you run Docker Compose or a small Kubernetes cluster and want minimal configuration ceremony. Not when: you are at a scale where you need to reason hard about tail latency.
Caddy occupies a similar niche with the strongest automatic-HTTPS story of any server (certificates are issued and renewed with zero configuration); **Apache httpd with mod_proxy_balancer still exists and works, but its process/thread model makes it a poor fit for high-concurrency proxying and there is no reason to choose it for new work; Varnish** is a caching reverse proxy that does balance across backends but whose real purpose is HTTP caching (File 02) — do not select it as a load balancer, select it as a cache that happens to have one.
9.3Family 3 — Managed cloud load balancers#
Here you buy the operation, not the software. The critical thing to internalize is that cloud providers sell several distinct products at different layers, and picking the wrong one is a common and expensive mistake.
AWS Application Load Balancer (ALB) — Layer 7 only. Terminates TCP and TLS, parses HTTP/HTTPS/HTTP-2/gRPC/WebSocket, and routes on host header, URL path, HTTP method, query string, source IP, and arbitrary headers. It integrates with AWS Certificate Manager (free managed TLS certificates with automatic renewal), AWS WAF (web application firewall), Cognito (authentication at the edge), and can target EC2 instances, IP addresses, Lambda functions, and ECS/EKS containers. It supports weighted target groups, which is how you do canary and blue-green deploys (§2.9) natively. It performs active HTTP health checks. Its properties follow from being a full proxy: it cannot preserve the client's source IP at the TCP layer (the backend sees the ALB's IP; the real client IP arrives in the X-Forwarded-For header, §11.1), it adds a millisecond or two of latency, its throughput scales elastically but not instantly (very sharp spikes require pre-warming or a design that tolerates a scaling lag), and it only speaks HTTP-family protocols. Pricing model: an hourly charge plus LCU (Load Balancer Capacity Units), a composite metric billing on whichever of new connections/sec, active connections, processed bytes, or rule evaluations is highest — meaning cost tracks your traffic shape, not just its volume.
AWS Network Load Balancer (NLB) — Layer 4 only. Operates on TCP, UDP, and TLS at the connection level. It is built on AWS's Hyperplane distributed data plane and delivers ultra-low latency (tens of microseconds of added latency), millions of requests per second, and instant scaling with no pre-warming. It can give you a static IP per availability zone (and supports Elastic IPs), which is exactly what you need when a customer must whitelist your address in their firewall. Critically, it preserves the client source IP for instance targets — the backend genuinely sees the client's address — because it does not proxy in the ALB sense. It can also terminate TLS if you want that at L4. Limits, all following from being L4: no path or header routing, no X-Forwarded-For insertion (use Proxy Protocol v2 if you need client info with IP targets), no HTTP-level retries, no WAF integration, no request-level metrics. Pricing: hourly plus NLCUs, and generally cheaper than ALB at high byte volumes.
AWS Gateway Load Balancer (GWLB) — the one people forget. It is not for balancing your application at all; it exists to insert third-party virtual network appliances (firewalls, IDS/IPS, deep packet inspection) transparently into your traffic path. It operates at Layer 3, uses the GENEVE encapsulation protocol on port 6081 to hand packets to appliance fleets while preserving the original packet, and keeps flows pinned to one appliance instance. Reach for it when a security team needs all VPC traffic to traverse an inspection fleet, and you need that fleet to be horizontally scalable and health-checked. Mentioning GWLB when an interviewer asks "what AWS load balancers exist" is a strong signal you know the platform rather than the two famous names.
AWS Classic Load Balancer (CLB) is the deprecated original, spanning a limited L4/L7 hybrid. It exists only in old accounts. Do not choose it for new work; know it exists so you recognize it in a legacy diagram.
GCP Cloud Load Balancing is architected differently from AWS's in a way worth knowing: its Global External HTTP(S) Load Balancer is a single anycast IP advertised from Google's edge worldwide, so a user in Tokyo and a user in Frankfurt hit the same address and are absorbed at their nearest edge, then carried to the backend region over Google's private backbone — no DNS-based geo-routing, no per-region addresses, and failover between regions with no DNS change. AWS's equivalent capability requires composing CloudFront and/or Global Accelerator with regional ALBs. GCP's Network Load Balancer is the passthrough L4 product built on Maglev (§9.1), and it uses DSR-style return, which is why it preserves client IP and adds almost no latency.
Azure splits the same way: Azure Load Balancer (L4, regional), Application Gateway (L7, regional, with an integrated WAF), and Azure Front Door (L7, global, anycast, CDN-integrated).
9.4Family 4 — Edge / CDN / global traffic management#
Cloudflare, Fastly, Akamai, AWS CloudFront + Global Accelerator. These are load balancers whose distinguishing property is where they run: hundreds of points of presence worldwide, absorbing traffic within milliseconds of the user. They provide anycast VIPs, DDoS absorption at multi-terabit scale (a capability no single-region LB can offer, because absorbing a volumetric attack requires more ingress capacity than the attack has egress), TLS termination at the edge, and origin load balancing with health checks and geo-failover. Fastly's differentiator is instant (sub-second) cache purging and VCL-based edge programmability; Cloudflare's is the breadth of its free tier and Workers (edge compute); Akamai's is the largest and most deeply embedded PoP footprint. Reach for these when your users are global, or when you need DDoS protection, or when static/media content dominates your response bytes. They are covered fully in File 05 (CDNs) — the load-balancing angle is simply that geographic routing is load balancing with distance as a cost function.
DNS-based global load balancing (Route 53 latency/geolocation/weighted routing, NS1, Akamai GTM) deserves a distinct mention because it is a load balancer that owns no data path at all: it makes its decision at name-resolution time by handing different clients different IP addresses. Benefit: free, infinitely scalable, works for any protocol. Cost: DNS caching means failover is bounded by TTL and by resolvers that ignore TTLs, so it is a coarse instrument — minutes, not milliseconds — and it balances resolvers, not users, so a single large ISP resolver can steer a whole population at once. Mitigation: short TTLs plus health-checked records, or move the job to anycast, which fails over at BGP speed rather than DNS speed.
9.5Family 5 — Kubernetes and service-mesh load balancing#
In Kubernetes there are four distinct load-balancing layers, and conflating them is one of the most common sources of confusion:
- **
kube-proxy/ Service (ClusterIP)** — the in-cluster L4 balancer. Each Service gets a virtual IP, andkube-proxyprograms either iptables rules (which pick a backend pod using probabilistic rules, effectively random selection) or, inipvsmode, IPVS itself (§9.1), which gives real algorithms and much better performance above a few hundred services because IPVS uses hash lookups where iptables walks a linear rule chain. - **Service type
LoadBalancer** — asks the cloud provider to provision one of the managed products in §9.3 and point it at the nodes. This is the bridge from cluster to internet, and it is why one Service of this type per microservice becomes expensive fast. - Ingress / Gateway API — an L7 router inside the cluster (implemented by ingress-nginx, Traefik, HAProxy Ingress, Envoy Gateway, or a cloud controller), letting many hostnames and paths share a single external load balancer. The newer Gateway API is the successor to Ingress, with a role-oriented resource model and richer routing.
- Service mesh (Istio/Envoy, Linkerd) — a sidecar proxy next to every pod that balances outbound calls at L7 with retries, circuit breaking, mTLS, and per-request observability. Linkerd's
linkerd2-proxyis a lightweight Rust proxy notable for using EWMA (exponentially weighted moving average) latency-aware balancing with power-of-two-choices (§4.8) rather than round robin. This is File 10's territory.
Client-side load balancing belongs here too: with gRPC's built-in balancing, or the older Netflix Ribbon, the client holds the list of backends (from a registry like Eureka or Consul) and picks one itself, with no middlebox at all. Benefit: one fewer network hop, no LB tier to scale, and it solves gRPC's connection-multiplexing problem naturally. Cost: every client must embed the logic (hard in a polyglot fleet), and rolling out a balancing change means redeploying every client. Mitigation: the sidecar model — put the logic in a proxy next to the client instead of inside it, which is exactly the argument for a service mesh.
9.6The comparison table#
| Product | Layer | Forwarding mode | Config / reload story | TLS | Protocols | Model | Best for |
|---|---|---|---|---|---|---|---|
| LVS/IPVS | L4 | NAT, DR, TUN | ipvsadm + keepalived; imperative | ❌ none | TCP/UDP | Self-hosted, free | Bare-metal high-throughput L4 tier |
| Maglev (GCP NLB) | L4 | GRE + DSR | Managed | ❌ (passthrough) | TCP/UDP | Managed | Planet-scale L4, client-IP preservation |
| Katran / Unimog | L4 | XDP/eBPF + encap | Code + control plane | ❌ | TCP/UDP | Self-hosted, free | Custom hyperscale L4 |
| F5 / Citrix ADC | L4+L7 | Full proxy, DSR | GUI/CLI/iRules | ✅ hardware-accelerated | Everything | Appliance, $$$$ | Enterprise/regulated, feature depth |
| NGINX | L4+L7 | Full proxy | Config file + graceful reload (API in Plus) | ✅ | HTTP/1.1, /2, /3, gRPC, TCP, UDP | Self-hosted, free/paid | All-in-one server + proxy + cache |
| HAProxy | L4+L7 | Full proxy | Config file + runtime admin socket (no reload) | ✅ | HTTP/1.1, /2, gRPC, TCP | Self-hosted, free | Health checks, draining, observability |
| Envoy | L4+L7 | Full proxy | Dynamic xDS from a control plane | ✅ | HTTP/1.1, /2, /3, gRPC, TCP | Self-hosted, free | Meshes, gRPC, churning backends |
| Traefik | L7 | Full proxy | Auto from Docker/K8s labels | ✅ auto-ACME | HTTP, gRPC, TCP | Self-hosted, free | Container platforms, low ceremony |
| AWS ALB | L7 only | Full proxy | AWS API / console | ✅ ACM + WAF | HTTP/1.1, /2, gRPC, WS | Managed, LCU-priced | HTTP routing, canary weights, Lambda targets |
| AWS NLB | L4 only | Passthrough (Hyperplane) | AWS API / console | ✅ optional | TCP, UDP, TLS | Managed, NLCU-priced | Static IPs, client-IP preservation, µs latency, spikes |
| AWS GWLB | L3 | GENEVE to appliances | AWS API | ❌ | All IP | Managed | Inserting firewall/IDS fleets |
| GCP Global HTTP(S) LB | L7 | Anycast + backbone | GCP API | ✅ managed certs | HTTP family | Managed | One global IP, cross-region failover |
| Cloudflare / Fastly / Akamai | L7 edge | Anycast full proxy | Dashboard / API / VCL / Workers | ✅ | HTTP family | SaaS | Global users, DDoS, caching |
| Route 53 / NS1 (DNS GLB) | "L0" | No data path | Records + health checks | n/a | Any | Managed | Coarse geo/failover steering |
| kube-proxy (IPVS mode) | L4 | NAT/DR in kernel | Kubernetes API | ❌ | TCP/UDP | In-cluster | Pod-to-pod Service VIPs |
| Istio/Envoy, Linkerd | L7 sidecar | Full proxy per pod | Control plane | ✅ mTLS | HTTP, gRPC | In-cluster | Per-request policy, retries, mesh |
9.7Decision rules — say these out loud in an interview#
- Choose NLB over ALB when you need a static IP for customer firewall whitelisting, when the backend must see the real client IP at the TCP layer, when you serve a non-HTTP protocol (raw TCP, UDP, MQTT, a database port), when traffic spikes violently and you cannot wait for a proxy tier to scale, or when you need the absolute lowest latency. Choose ALB over NLB when you need routing by path or host, TLS termination with managed certificates, weighted target groups for canary deploys (§2.9), WAF integration, Lambda targets, or HTTP-level health checks and metrics.
- Use both, stacked, when you want each one's strength: NLB at the edge for a static IP and instant spike absorption, ALB behind it for HTTP routing. This is the pattern in §10.3, and knowing why the stack exists — not just that it does — is the senior answer.
- Choose HAProxy over NGINX when load balancing is the entire job and you need sophisticated active health checks, runtime weight/drain control without reloading, or fine-grained latency breakdown in logs. Choose NGINX over HAProxy when you also want a web server, static file serving, or an HTTP cache in the same process, or when your team already knows it — operational familiarity is a real engineering input, not a soft one.
- Choose Envoy over both when your backend set changes continuously (autoscaling microservices), you need gRPC-aware balancing, or you are adopting a mesh. Do not choose Envoy for a static three-server deployment; you will spend your time on a control plane you did not need.
- Choose LVS/IPVS or an eBPF balancer when you are on bare metal, response bytes vastly exceed request bytes, and the L2 topology is yours to control. Choose a cloud L4 product instead when you are in a VPC, because the L2 assumptions of DSR do not hold there (§2.8.2).
- Choose an edge/CDN provider when users are geographically spread, when static or media bytes dominate, or when DDoS resilience is a requirement — no origin-region load balancer can absorb a volumetric attack.
- Choose DNS-based global balancing only for coarse steering, never for fast failover, because TTL caching bounds your recovery time. Prefer anycast when you need both global reach and fast failover.
9.8Anti-recommendations — the common wrong picks#
- Reaching for an ALB because "it's the default" when the workload is not HTTP. Every gRPC-over-raw-TCP, MQTT, game-server, or database-proxy workload put behind an ALB either fails outright or gets mangled. The tell that you have made this mistake is fighting the LB's protocol assumptions.
- Assuming the backend sees the client IP. Behind any full proxy it does not — it sees the proxy's IP. Rate limiting (File 03), geo-logic, and abuse detection built on the wrong IP will silently treat your entire user base as one client. Use
X-Forwarded-Forat L7 or Proxy Protocol at L4, and validate the header rather than trusting it blindly (§11.1). - **One
type: LoadBalancerService per microservice in Kubernetes.** Each provisions a separate cloud load balancer with its own hourly charge and its own IP. Forty microservices become forty load balancers. Use an Ingress/Gateway to share one. - Choosing a service mesh to get load balancing. A mesh is a large operational commitment — a control plane, a sidecar per pod, certificate rotation, upgrade choreography. Adopt it for mTLS, uniform observability, and per-request policy across many services; if all you needed was balancing, an Ingress or client-side balancing is a tenth of the cost.
- Picking DSR because it is clever. DSR's economics only pay when response bytes vastly exceed request bytes. If your service returns JSON, the response path is not your bottleneck, and you have taken on the L2 constraint and the ARP configuration hazard of §2.8 for nothing.
- Running exactly one load balancer. Whatever product you chose, if there is one instance, you moved the single point of failure rather than removing it (§7.2).
10Real-World Engineering Scenarios#
10.1Netflix — Zuul + Eureka (L7, self-healing edge)#
Netflix's edge gateway is Zuul, a Layer 7 load balancer/router they open-sourced. End-to-end flow:
- A device (TV, phone) resolves an Amazon Route 53 DNS name and hits an AWS ELB, which fronts a fleet of Zuul instances.
- Zuul performs L7 routing: it inspects the request, applies filters (auth, decoration, throttling), and routes to the correct backend microservice cluster.
- Backends register themselves in Eureka, Netflix's service discovery registry (a "phone book" of live service instances). Zuul asks Eureka "who are the healthy instances of the
playbackservice?" and load-balances across them client-side using Ribbon, which implements algorithms like weighted-response-time and zone-aware routing. - Netflix pioneered client-side load balancing at scale: instead of a central LB choosing the backend, the caller (Zuul/Ribbon) holds the server list and chooses — removing a central chokepoint and enabling per-caller strategies. They combine this with Hystrix circuit breakers (File 10) so a failing backend is shed instantly.
- Takeaway: massive-scale L7 balancing + service discovery + client-side selection + circuit breaking, all cooperating.
10.2Google — Maglev (software L4 at planet scale)#
Google's Maglev is a software Layer 4 load balancer running on commodity servers (published paper, 2016).
- Traffic to a Google VIP is spread across many Maglev machines using two network-layer techniques worth defining. Anycast (deep dive in File 05): the same IP address is advertised from many locations at once, and internet routing (BGP) naturally delivers each client's packets to the topologically nearest one — so a user in Tokyo and a user in Paris both send to the same VIP but land in different datacenters. ECMP (Equal-Cost Multi-Path): within the datacenter, the router has multiple equally-good next hops (the many Maglev boxes) for that VIP, and it hashes each packet's flow 5-tuple to pick one — same flow always hashes to the same Maglev, so TCP connections stay intact while load spreads across the whole Maglev tier. Net effect: one VIP, planet-wide, with no single ingress box.
- Each Maglev box independently and consistently hashes each connection's 5-tuple to a backend using a specialized consistent hashing scheme (Maglev hashing) — so even though many Maglev boxes see the traffic, they all agree on which backend a given connection maps to, keeping connections pinned without shared state.
- It uses kernel-bypass techniques (userspace packet processing, no per-packet syscalls) to hit line-rate throughput on ordinary hardware.
- Takeaway: L4 + consistent hashing + anycast + kernel bypass = one VIP absorbing millions of packets/sec across a fleet of cheap boxes, replacing expensive hardware LBs.
10.3AWS — ALB + NLB Stack for a SaaS App#
A typical production topology on AWS:
- Route 53 (DNS) resolves
app.company.comto the LB. For global apps, Route 53 latency-based / geolocation routing sends users to the nearest region. - At the edge, an NLB (Layer 4) provides static IPs, handles millions of connections, preserves source IP, and gives DDoS resilience (with AWS Shield).
- The NLB forwards to an ALB (Layer 7) which terminates TLS (certs from ACM), routes by path/host to different target groups (containers in ECS/EKS), does cookie stickiness where needed, integrates a WAF, and emits per-route CloudWatch metrics.
- Target groups run active
/healthzchecks; unhealthy tasks are drained (deregistration delay) during deploys. Auto Scaling adds/removes targets based on CPU/RPS, and the ALB picks them up automatically. - Takeaway: the canonical stacked L4→L7 pattern, fully managed, integrated with autoscaling and health checks.
11Interview Gotchas & Failure Modes#
11.1Classic Interviewer Questions (and the crisp answers)#
- "Is the load balancer a single point of failure?" — Yes, if you run one. Real deployments run redundant LBs: active-passive with VRRP/keepalived and a floating VIP (heartbeats + gratuitous ARP takeover, explained in §7.2), or active-active behind anycast/ECMP (defined in §10.2 — many LB boxes all legitimately answering for the same VIP, with the network spreading connections across them), or a managed cloud LB that's internally multi-AZ redundant. Always mention this.
- "L4 or L7 — pick and justify." — Decide by whether you need content awareness (→L7) or raw speed/protocol independence (→L4). Bonus: mention stacking them.
- "How do you deploy with zero downtime?" — Connection draining + rolling deploy (or blue-green / canary via weighted routing). Drain old, health-check new, shift traffic.
- "How does the backend learn the real client IP if the LB is a proxy?" — The problem: a full proxy (or full-NAT, §3.2) replaces the source IP with its own, so the backend's logs, rate limits, and geo checks all see the LB's IP. Fixes by layer — L7: the LB appends the real client IP to the
X-Forwarded-For/Forwardedheaders (plusX-Forwarded-Protofor the original scheme) as it forwards the HTTP request. L4: the LB can't add HTTP headers (it doesn't parse HTTP), so it uses Proxy Protocol — a small preamble the LB prepends to the TCP byte stream before any application data, carrying the original source/destination IPs and ports; the backend (Nginx, HAProxy, and most servers support it) reads and strips that preamble on accept, recovering the true client identity. Both sides must agree it's enabled — a backend not expecting the preamble sees garbage bytes and breaks. Or use DSR, which never rewrites the source IP in the first place. - "How do you scale the load balancer itself?" — DNS round robin across multiple LB VIPs, or anycast/ECMP spreading connections across an LB cluster, or a managed LB that auto-scales its own capacity units.
11.2Failure Modes & Mitigations#
- The "all backends unhealthy" cascade. A bug in the health check endpoint (e.g.,
/healthzalso pings a now-down dependency) makes every server report unhealthy → LB has zero servers in rotation → total outage, even though the app itself is fine. Mitigation: "fail open" / panic mode — if more than X% of the fleet is unhealthy, the LB ignores health checks and sends traffic to everyone anyway (Envoy's panic threshold), on the theory that overwhelmed-but-serving beats zero-serving. Also: keep health checks shallow enough not to couple to every dependency. - Thundering herd on failover. A server dies; its load slams onto the survivors, which then also tip over → cascading failure. Mitigation: headroom (run below capacity), load shedding, circuit breakers, autoscaling reaction speed.
- Retry storms. The LB retries failed requests; if a backend is slow (not failed), retries multiply load and make it worse. Mitigation: retry budgets (cap retries to e.g. 10% of traffic), exponential backoff + jitter, only retry idempotent requests.
- Sticky-session hot spots. Stickiness pins heavy users to one server, creating imbalance. Mitigation: externalize state (statelessness).
- LB failover drops connections. When an L4 LB fails over to its standby, the conntrack table may be lost → in-flight TCP connections reset. Mitigation: connection state sync between LB pair, or accept that long connections re-establish; consistent hashing lets a fresh LB re-derive the same backend mapping without shared state.
- TLS/cert expiry. An expired certificate at the LB = instant total outage that looks like a network problem. Mitigation: automated cert rotation (ACM, cert-manager) + expiry alerting.
- Uneven long-lived connections (WebSockets). Round robin balances new connections, but WebSockets live for hours, so an old server accumulates connections while newly-added servers stay empty. Mitigation: least-connections algorithm + periodic connection rebalancing / max-connection-age.
12Whiteboard Cheat Sheet#
┌────────────────────────┐
Clients ───DNS──► │ Redundant LB pair/VIP │ (never a single box!)
│ ┌──────────────────┐ │
│ │ L4 (NLB) edge │ │ fast, TCP/UDP, static IP, DDoS
│ └───────┬──────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ L7 (ALB/Envoy) │ │ TLS term, path/host routing,
│ └───────┬──────────┘ │ retries, WAF, metrics
└──────────┼──────────────┘
health checks ┌───────┼───────┬───────────┐
(/healthz) ▼ ▼ ▼ ▼
[srv1] [srv2] [srv3] ... [srvN] ◄─ stateless!
session in Redis
Algorithm picks: RR / weighted-RR / least-conn / least-latency / P2C / consistent-hash
Deploy safely: drain → deploy → healthcheck → re-add (rolling / blue-green / canary)
Failure guards: panic mode, retry budgets, circuit breakers, headroom, autoscaleThe one-sentence summary to give an interviewer:
"A load balancer is the entry point that makes horizontal scaling and high availability possible; choose L4 for raw speed and non-HTTP protocols and L7 for content-aware routing and HTTP features, run it redundantly so it's not a SPOF, keep your servers stateless so you don't need stickiness, and defend against cascading failure with health checks, panic mode, retry budgets, and circuit breakers."
End of File 01. Next up → File 02: Caching Strategies (write-through, write-back, cache-aside, eviction policies, and the thundering-herd problem). Prompt me to continue.