Load balancing: L4 vs L7
Load balancing for system design interviews: L4 vs L7 load balancers, round robin, least connections, consistent hashing and power of two choices, health checks, sticky sessions, connection draining, global DNS and anycast balancing, client-side gRPC balancing and keeping the load balancer from being a single point of failure.
On this page
- Why load balancers exist
- L4 vs L7: what the balancer can see
- Algorithms: how the next server is picked
- Health checks: active vs passive
- Sticky sessions, and why to avoid them
- Connection draining
- Global vs regional load balancing
- Client-side load balancing
- The load balancer as a single point of failure
- Practise: pick the layer
- How it shows up in design problems
- Interview follow-ups
- How to say it in the interview
⚖️ What is a load balancer?
A load balancer is the component that takes incoming traffic for one address and spreads it across a pool of servers doing the same job, skipping any that are unhealthy. If api.example.com runs on 12 identical instances, the load balancer is why clients see one name and the work is shared twelve ways. Picture the host at a busy restaurant: guests don't pick a waiter, the host seats each party where there's room, and stops seating anyone in a section whose waiter went home sick.
For this lesson, we'll cover the L4 vs L7 split, algorithms, health checks, sticky sessions, draining, global and client-side balancing, and how the balancer avoids being the weak link. Interviewers ask about it constantly because "add a load balancer" is the first scaling move in nearly every design, and the follow-up ("which kind, which algorithm, what if it dies?") shows who has run one.
Below the line (out of scope)
- What a reverse proxy does beyond balancing (buffering, X-Forwarded-For, 502 vs 504): Proxy vs reverse proxy
- Picking a region by name: Domain Name System (DNS)
- The hashing ring itself: Consistent hashing
- How clients find instances: Service discovery
The 30-second answer
A load balancer gives a pool of servers one address, spreads traffic across them and routes around failures. An L4 balancer works on TCP or UDP connections: it never reads the payload, so it's very fast and protocol-agnostic, but every request on a connection goes to the same backend. An L7 balancer terminates TLS and reads HTTP, so it can route by path or header, retry, and balance each request separately, at more CPU per request. I start with round robin for identical stateless servers, use least connections when request times vary, and consistent hashing when a key should land on the same node. Health checks pull bad servers out, draining lets deploys finish in-flight work, and the balancer itself runs as a redundant tier, never one box.
What you’ll learn
- Explain L4 vs L7 load balancing and pick the right layer for HTTP, gRPC, WebSockets and UDP
- Choose an algorithm (round robin, least connections, consistent hashing, power of two choices) to match the traffic
- Tune active and passive health checks and work out how long a dead server keeps getting traffic
- Replace sticky sessions with stateless servers, and drain connections safely during deploys
- Layer global (DNS, anycast) and regional balancing, and make the balancer tier itself redundant
01Why load balancers exist
Four jobs, each building on the last:
- Scale out. Ten stateless servers behind a balancer handle roughly ten times the traffic of one.
- Survive failures. Health checks pull a dead instance, so a crash is a capacity dip, not an outage.
- Change without downtime. Deploys, canaries and autoscaling add and remove pool members behind a stable address.
- One front door. Clients know one name; the fleet behind it can change every minute.
02L4 vs L7: what the balancer can see
The layer number tells you how far into the packet the balancer looks, and that decides everything else. Here's the difference side by side.
What an L4 and an L7 load balancer can see
An L4 balancer looks at addresses, ports and protocol (the 5-tuple), picks a backend when a connection starts, and forwards every packet of that connection to the same place. It doesn't decrypt anything. AWS's Network Load Balancer is the classic managed example; AWS documents it as working at layer 4 and handling millions of requests per second. An L7 balancer finishes the TCP and TLS handshakes itself, parses each HTTP request, and opens its own connections to backends, which makes it a reverse proxy.
| L4 (transport) | L7 (application) | |
|---|---|---|
| Sees | IPs, ports, TCP/UDP | Method, path, host, headers, cookies |
| Decides | Once per connection | Per request (or per HTTP/2 stream) |
| TLS | Passes it through (or terminates, in some products) | Terminates it |
| Protocols | Anything over TCP or UDP | HTTP/1.1, HTTP/2, gRPC, WebSocket upgrade |
| Can do | Very high throughput, low added latency, keep the client IP | Path and header routing, retries, rewrites, auth hooks, per-request metrics |
| Costs | No content-aware routing; long connections pin load | More CPU per request; another TLS hop to manage |
| Typical product | AWS NLB, Maglev, IPVS | AWS ALB, nginx, Envoy, HAProxy in HTTP mode |
The gRPC team's own guidance puts the trade-off neatly: pick L4 when latency or proxy cost matters most, and L7 when load varies a lot between connections, because only L7 can spread the streams of one HTTP/2 connection across several backends.
03Algorithms: how the next server is picked
There's no best algorithm, only the one that matches your traffic. This table is what I'd want a candidate to be able to reproduce.
| Algorithm | How it picks | Fits when | Watch out for |
|---|---|---|---|
| Round robin | Next server in turn | Identical servers, short, similar requests | Ignores how busy each server is |
| Weighted round robin | In proportion to weights | Mixed instance sizes, sending 5% to a canary | Weights are static guesses |
| Least connections | Fewest active connections | Request times vary, long-lived connections (WebSockets) | A fresh server gets stampeded; use slow start |
| Least response time | Lowest recent latency (and connections) | Backends with uneven performance | A server failing fast looks fast; combine with error checks |
| IP hash | Hash of the client IP | Cheap affinity without cookies | A whole office behind one NAT lands on one server |
| Consistent hashing | Hash a key (user ID, cache key) onto a ring | Caches and per-key state; adding a node moves only about 1/N of keys | A hot key is still hot; see Consistent hashing |
| Power of two choices | Pick two servers at random, use the less loaded | Many balancers, each with a partial or stale view | Needs some load signal per server |
The last one shows depth. If 20 balancer instances run "least connections" on stale counts, they all spot the same idle server and pile onto it. Sampling two at random and taking the better one keeps most of the benefit while making that herd very unlikely.
All of these are one line in nginx. The upstream docs list least_conn, ip_hash (which hashes the first three octets of an IPv4 address), hash $key consistent (ketama consistent hashing) and random two least_conn (power of two choices). Round robin is the default.
upstream orders_api {
least_conn; # or: random two least_conn;
server 10.0.1.11:8080 weight=2; # bigger box, twice the share
server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
server 10.0.1.13:8080 backup; # only used if the others are down
}
# For a cache tier, pin each key to a node instead:
# upstream cache_tier { hash $arg_key consistent; server ...; }
04Health checks: active vs passive
A balancer is only as good as its idea of which servers are alive. There are two ways to know.
- Active checks: the balancer probes each target on a schedule (TCP connect, or
GET /healthexpecting a 200) and flips its state after a run of failures or successes. - Passive checks: the balancer watches real traffic and ejects a server that keeps failing it. nginx's
max_failsandfail_timeoutwork this way (defaults: 1 failure within 10 seconds marks the server unavailable for 10 seconds), and Envoy calls it outlier detection.
Use both: active checks catch a server before users hit it, passive checks react within a few requests. Make the health endpoint check what the instance needs to serve (can it reach its database?), but don't make it fail when a shared dependency is down, or every instance goes unhealthy at once and the balancer has nothing left to route to.
Taken out by health checks vs drained on purpose
Do the detection maths with real defaults. An AWS Application Load Balancer checks every 30 seconds with a 5-second timeout, marks a target unhealthy after 2 consecutive failures and healthy again after 5 consecutive passes. So a target that dies just after a good check keeps getting traffic for about 65 seconds (two intervals plus a timeout), and a recovered one waits about 150 seconds to return. If that's too slow, shorten the interval or lean on passive checks, which react on the first failed requests.
05Sticky sessions, and why to avoid them
Sticky sessions pin a user to one backend, usually with a cookie the balancer sets, to rescue apps that keep session state in memory. I treat them as a smell:
- Uneven load. A few heavy users stuck to one box, and you can't move them.
- Failures lose state. When that server dies, its users lose their sessions anyway.
- Scaling doesn't help existing users. New instances only get new sessions.
- Deploys drag. Draining waits on users who are pinned there.
The better default is stateless servers with sessions in a shared store such as Redis, or in a signed token. Affinity is fine as a performance hint (a user's data usually warm in one node's cache), not as a correctness requirement. WebSockets are sticky by nature, so plan for them to drop and reconnect.
06Connection draining
Removing a server on purpose shouldn't break its requests. Draining (AWS calls it deregistration delay) stops new requests to the target while in-flight ones finish; AWS waits 300 seconds by default. Set it a bit longer than your slowest normal request. For WebSockets and streams, drain for a while, then close them so clients reconnect elsewhere with backoff.
07Global vs regional load balancing
Everything so far happens inside one region. In front of that sits a second, coarser decision: which region should this user reach? Here's the full stack.
Global, then regional: the load balancing stack
| Global method | How it works | Good at | Catch |
|---|---|---|---|
| DNS-based (GSLB) | The DNS answer depends on the user's location, measured latency or region health | Works with any protocol; simple; per-region weights | Resolvers and clients cache answers until the TTL expires, so failover is as slow as caching allows; see DNS |
| Anycast | Many sites announce the same IP over BGP, and internet routing delivers each user to a nearby one | Fast failover (withdraw the route), absorbs DDoS across sites | Routing can shift mid-connection; you need your own network presence; see IP addresses |
Large services combine them, then balance inside the region. Multi-region and active-active covers the data side.
08Client-side load balancing
Inside a microservice system, there's often no balancer box at all. Each client gets the instance list from service discovery and picks one per call: one hop fewer and no central bottleneck, at the cost of balancing logic in every client (or a sidecar such as Envoy).
gRPC is where this matters most. A gRPC client multiplexes all its calls over one long-lived HTTP/2 connection, so behind an L4 balancer every call from that client lands on one backend, and adding servers doesn't spread existing load. gRPC's default pick_first policy has the same effect. Switching to round_robin makes the client connect to every address the resolver returns and rotate per call:
ManagedChannel channel = ManagedChannelBuilder
.forTarget("dns:///orders.internal:8443") // resolver returns every instance
.defaultLoadBalancingPolicy("round_robin") // default is pick_first
.build();
The alternatives are an HTTP/2-aware L7 balancer or a service mesh. The gRPC team recommends client-side balancing for trusted, high-traffic clients and a proxy when you need a trust boundary.
09The load balancer as a single point of failure
If every request goes through one balancer, you've moved the single point of failure, not removed it. Two patterns fix that:
- Active-passive pair: two balancers share a floating virtual IP (VRRP, for example via keepalived); the standby takes the IP when the active one stops sending heartbeats. Simple, but half the hardware idles and the pair's capacity is one box.
- Active-active tier: many balancer machines, all serving. Upstream routers spread flows across them with ECMP, or anycast spreads them across sites. Losing one costs 1/N of capacity.
Managed cloud balancers do this for you: AWS documents that enabling an Availability Zone creates a load balancer node in that zone, and the balancer's DNS name resolves to the nodes. Your job is to enable at least two zones and keep enough headroom in each.
Google's Maglev, described in an NSDI 2016 paper, is the active-active pattern at full scale. It's a software L4 balancer on ordinary Linux servers that had been serving Google's traffic since 2008. Routers spread packets across Maglev machines with ECMP; each machine picks a backend, wraps the packet in GRE and sends it on, and the backend replies directly to the user (direct server return), because responses are much larger than requests. The paper notes that hardware balancers are usually deployed as active-passive pairs, while Maglev runs every machine active for N+1 redundancy. Its consistent hashing plus connection tracking means that when a Maglev machine is added or lost, packets of existing connections still reach the same backend. A single machine can saturate a 10 Gbps link with small packets. Cloudflare's Unimog takes the idea further and makes every edge server part of the L4 balancer.
10Practise: pick the layer
Try it yourself first
Before reading on, choose L4 or L7 (or "either") for each, and say why in one sentence: WebSockets for a chat app, gRPC between internal services, UDP traffic for a multiplayer game, and path-based routing so /api and /images go to different services.
Here's how I'd answer out loud.
| Traffic | Layer | Why |
|---|---|---|
| WebSockets | Either; L7 if you route or authenticate on the upgrade | It starts as an HTTP request, so L7 can route by path and check auth; after that it's one long connection, so balancing happens once either way. Use least connections, raise idle timeouts and drain carefully |
| gRPC | L7 or client-side | One HTTP/2 connection carries every call; L4 pins them all to one backend. L7 (or a round_robin client) spreads individual calls |
| Game UDP | L4 | There's no HTTP to read, and an L7 proxy would only add latency. Hash on the 5-tuple (or a session ID in the packet) so a player's packets stay on one server |
| Path-based routing | L7 only | The path lives inside the HTTP request, which an L4 balancer never reads |
11How it shows up in design problems
- Design a URL shortener: stateless app servers behind an L7 balancer with round robin; the redirect path is short and uniform.
- Design WhatsApp: millions of long-lived WebSocket connections, so least connections, draining plans and a mapping of which server holds which user's connection.
- Design an LLM gateway: request cost varies by orders of magnitude, so least outstanding requests or power of two choices beats round robin.
- Design a distributed cache: consistent hashing on the key, so a node change only moves a slice of the cache.
- Design a rate limiter: hash the user or API key to one limiter node so its counter isn't split across many.
At this point, you should ask your interviewer whether servers hold any per-user state. That one answer decides between round robin, hashing and sticky sessions.
12Interview follow-ups
What if the interviewer asks why not use DNS round robin instead of a load balancer?
DNS can spread users across a few addresses, but it can't react quickly: answers are cached until the TTL expires, clients often stick to the first address, and DNS has no idea whether a server is overloaded. It's a good fit for picking regions or balancer nodes, and a poor fit for picking individual app servers.
What if a new instance joins and immediately gets flooded?
That's least connections treating an empty server as the best target, while its caches and JIT are still cold. Use slow start (ramp its weight up over a minute or two) or power of two choices, and make readiness checks pass only once it's warmed up.
What if your health checks all pass but users are seeing errors?
The check is too shallow, for example it returns 200 whenever the process is up. Add passive checks that eject servers returning 5xx on real traffic, and make the active check exercise what a request needs. Then check whether the errors come from a shared dependency, which no amount of rerouting will fix.
What if the whole region's load balancer tier fails?
Global balancing takes over: DNS health checks or an anycast route withdrawal send users to another region. How fast depends on DNS TTLs or BGP convergence, and the other region needs headroom to absorb the traffic. Without that headroom, failing over just moves the outage.
13How to say it in the interview
When I ask about load balancing, I'm listening for four things: that you pick a layer for a reason, that your algorithm matches the traffic, that you know how unhealthy servers get removed, and that the balancer itself isn't a single box.
In the interview
"Users first hit DNS or anycast, which sends them to a healthy region nearby. Inside the region, an L4 tier spreads TCP connections across a pool of L7 proxies, and the L7 layer terminates TLS and routes by path: /api to the API service, /media to the media service. The app servers are stateless, so I'd use round robin, or least connections for the endpoints with variable latency; sessions live in Redis, so no sticky sessions. Health checks probe /health every few seconds and need two failures before a server is pulled, with passive ejection on 5xx as a backstop. Deploys deregister instances and drain for about 30 seconds so in-flight requests finish. No tier is one machine: the L4 tier is active-active behind ECMP across at least two zones. For internal gRPC calls I'd balance per call on the client, because an L4 balancer would pin each client's HTTP/2 connection to one backend."
On the whiteboard
- Why: scale out · route around failures · zero-downtime deploys · one address
- L4 = 5-tuple, per connection, fast, any protocol · L7 = HTTP, per request, TLS, path routing
- Algos: RR · weighted · least conn · least time · IP hash · consistent hash · P2C
- Health: active probes + passive ejection · N failures out, M passes back
- Avoid sticky sessions → stateless + shared session store
- Drain on deploy (AWS default 300 s)
- Global: DNS (TTL) / anycast (BGP) → regional L4 → L7 → service
- gRPC: L7 or client-side round_robin, not L4
- LB redundancy: active-active + ECMP, ≥ 2 zones (Maglev N+1)
Further reading (6)
- nginx: ngx_http_upstream_module: Every common algorithm in one place, from least_conn and ip_hash to consistent hashing and random two, plus passive failure counting.
- Health checks for Application Load Balancer target groups (AWS docs): Real default intervals, timeouts and healthy and unhealthy thresholds to reason about detection time.
- Edit target group attributes: deregistration delay (AWS docs): How connection draining works on a managed balancer, with its 300-second default.
- gRPC Load Balancing (gRPC blog): Proxy vs client-side balancing and when L4 or L7 fits, from the people who built the protocol.
- Maglev: A Fast and Reliable Software Network Load Balancer (NSDI 2016): Google's active-active L4 tier: ECMP, consistent hashing, connection tracking and direct server return.
- Unimog: Cloudflare's edge load balancer (Cloudflare Blog): A layer 4 balancer built on XDP that turns every edge server into part of the balancer.