LoadBalancer Interview Questions | JiQuest

add

#

LoadBalancer

System design deep dive · HLD

Design a Load Balancer

Distribute millions of connections across a fleet of backends, detect failures in seconds, and never become the single point of failure yourself — the HAProxy / Envoy / AWS NLB problem, end to end.

2M+Conns/sec, LB tier
2sHealth-check interval
3,000Backends per pool
Client Load Balancer round-robin Health Checker Backend A healthy Backend B healthy Backend C unhealthy — excluded 3 backends shown · pool scales to thousands

1. Requirements

Before any box-and-arrow diagram: what must this system guarantee, and what are we explicitly not building?

Functional requirements

Distribute trafficSpread incoming connections/requests across a pool of healthy backend instances using a configurable algorithm.
Health checkingContinuously verify backend liveness via active probes (synthetic pings) and passive signals (real traffic errors/timeouts).
L4 and L7 routingSupport raw TCP/UDP forwarding (L4) and content-aware routing on host/path/headers (L7).
Session affinityOptionally pin a client to the same backend (cookie or source-IP hash) for stateful protocols.

Non-functional requirements

No single point of failureThe LB tier itself must be redundant — losing one instance must not drop traffic.
Sub-millisecond overheadRouting decision and proxying add negligible latency versus a direct connection.
Graceful drainingDuring deploys, in-flight connections finish before a backend is removed — no dropped requests.
Massive connection scaleMillions of concurrent connections and hundreds of thousands of requests/sec per tier.
Explicitly out of scope Full API-gateway concerns — authentication/authorization, business-logic rate limiting, request transformation, and a Web Application Firewall are layered on top of (or beside) the load balancer, not inside it. We design the traffic-distribution and health-management core only.

2. Capacity planning

Rough numbers for a large multi-service platform — enough to size the LB tier and justify the health-check interval.

MetricEstimateNote
Peak concurrent connections~5,000,000Kept-alive HTTP/2 and long-poll/WebSocket connections across all services
Peak requests/sec~900,000 RPSBlended across L7 virtual hosts
Health-check overhead~18,000 pings/sec3,000 backends × 6 LB instances probing every 2s — under 2% of request volume
Backend instances per pool200 – 3,000Autoscaled; largest pools are stateless web/API tiers
LB throughput per instance~100,000 RPS / ~150,000 connsTypical sustained figure for a tuned HAProxy/Envoy instance on modern hardware
LB instances needed~10 active (12 provisioned)900K RPS ÷ 100K per instance, plus N+2 redundancy buffer
Why this matters The health-check interval is a trade-off, not a constant: too short and probes become a meaningful fraction of traffic; too long and a dead backend keeps receiving live requests. A 2s interval with a 3-failure threshold caps user-visible impact at roughly 6 seconds while keeping probe overhead under 2% of total load.

3. High-level design

Seven cooperating pieces: entry routing, an L4 tier, an L7 tier, the backend pool, a health checker, and a control plane that pushes configuration to all of them.

Clients DNS / Anycast entry routing L4 LB tier TCP/UDP, ECMP L7 LB tier host/path routing Backend Pool 100s–1000s of instances Health Checker active + passive Control Plane config store Solid = request path (flowing) · dashed = health/config path
L4 tier L7 tier Backend pool Health checker Control plane

What each box owns

L4 load balancer tier

Operates at the transport layer — forwards TCP/UDP packets based on IP/port without inspecting payload. Extremely fast (kernel-level or DPDK-accelerated) and protocol-agnostic. This is the outermost tier: an AWS Network Load Balancer or a Linux IPVS cluster fronted by ECMP routing.

L7 load balancer tier

Terminates the connection, parses HTTP/gRPC, and routes on host, path, headers, or cookies. This is where content-based routing, retries, and session affinity live — think Envoy, nginx, or an AWS Application Load Balancer sitting behind the L4 tier.

Health checker

Runs active probes (HTTP/TCP pings on an interval) against every backend and also watches passive signals from live traffic (5xx rate, timeout rate). Publishes a health verdict per backend that the routing layer consumes before every decision.

Control plane / config store

Holds the source of truth for backend pool membership, routing rules, and algorithm settings (etcd, Consul, or a cloud target-group API). Pushes updates to every LB instance so they converge without a restart.

4. Deployment architecture

A single LB instance is a liability. The real design spreads redundant LB instances across availability zones and makes the LB tier itself horizontally scalable and self-healing.

Region Anycast / GSLB entry AZ-1 L4 LB (keepalived/VRRP) L7 LB replica x3 (Envoy) Backend ASG — AZ-1 (autoscaling) AZ-2 L4 LB (keepalived/VRRP) L7 LB replica x3 (Envoy) Backend ASG — AZ-2 (autoscaling) Health-check sidecar (per L7 replica) local probe cache, 2s interval, 3-strike rule Config distribution (xDS / etcd watch) streams pool + rule updates to every replica Cross-AZ failover: VRRP VIP moves in <3s
DecisionChoiceWhy
L4 vs L7 placementL4 in front of L7L4 absorbs raw connection volume and DDoS-scale packet rates cheaply; only traffic that survives reaches the more expensive L7 parsing tier.
Default algorithmLeast-connections (L7), consistent hashing for cache-affinity poolsLeast-connections adapts to uneven request cost; consistent hashing keeps cache hit rates high when pool size changes.
Health-check strategyActive + passive, combinedActive probes catch a dead process fast even with zero live traffic; passive signals catch slow degradation active probes miss.
LB-tier HA mechanismECMP + VRRP/keepalived (self-hosted) or cloud-native NLB (managed)Removes the LB itself as a SPOF: a failed instance's VIP or ECMP route is withdrawn within seconds, no single box carries all traffic.

5. Sequence diagrams

Two flows that define correctness: how one request picks a backend, and how a failing backend gets pulled out of rotation (and later rejoins).

Sequence 1 — L7 request distribution

Client L7 LB Routing state Backend 1. HTTP request (Host, path, headers) 2. Parse path / host, match rule 3. Query healthy backend set 4. Return candidate list (health-filtered) 5. Apply algorithm, pick one backend 6. Forward request 7. Backend response 8. Response returned to client

The LB never guesses at backend health per-request — it consults the routing state that the health checker keeps fresh, filters to the healthy set, then applies the configured algorithm. Steps 3–5 typically run against an in-memory, per-instance cache of health state, not a network round trip, keeping this whole decision well under a millisecond.

Sequence 2 — Active health check failure and recovery

Health checker Backend Routing state On-call / alerts 1. Probe every 2s (TCP/HTTP ping) 2. Probe #1 fails (timeout) 3. Probe #2 fails 4. Probe #3 fails — threshold reached 5. Mark unhealthy, remove from rotation 6. Fire alert (backend down) 7. Probes resume, 2 consecutive successes 8. Mark healthy, re-add to rotation

Consecutive-failure thresholds (not single-probe) avoid flapping a backend out over one transient blip. Recovery is intentionally asymmetric — fewer successes are required to rejoin than failures were required to leave — because rejoining slowly costs capacity, while leaving slowly costs correctness.

6. Control-plane data model

A load balancer has no "business" schema — what it persists is its own operating state: which backends exist, whether each is healthy, and which routing rule sends traffic where.

backend_pools PK pool_id name algorithm min_healthy_pct created_at health_status PK backend_id FK pool_id endpoint_ip state (healthy/draining/down) consecutive_failures last_check_at routing_rules PK rule_id FK pool_id match_host / match_path priority 1 — N N — 1 One pool has many backends and many rules; each backend/rule belongs to exactly one pool.

Key modeling decisions

Health state is ephemeral, not historicalhealth_status stores current state only; a time-series of check results goes to metrics/logging, not the routing-critical table.
Rules carry priority, not order-of-insertionExplicit priority avoids subtle bugs when rules are added/removed concurrently by different operators.
Pools are the join pointBoth backends and rules reference pool_id so a pool can be resized or re-ruled without touching the other table.
Writes are rare, reads are constantEvery routing decision reads this data; config changes are comparatively infrequent — optimized for read-heavy, low-latency lookups.
StoreGood fit whenTrade-off
etcd / Consul (KV + watch)Every LB instance needs sub-second, push-based updates to convergeWeaker query flexibility than SQL; fine here since lookups are by key (pool_id, backend_id)
Relational DB (Postgres)An operator-facing control UI needs ad-hoc queries, joins, audit historyAdds a poll-or-invalidate step before changes reach the hot routing path — extra latency to converge
Hybrid (common in practice)Postgres as source of truth + etcd/xDS as the pushed, denormalized runtime viewTwo systems to keep in sync, but gives both queryability and fast propagation

7. Interview deep dive

L4 vs L7 — when do you actually need both?

L4 forwards packets by IP/port with no payload inspection: minimal CPU cost, protocol-agnostic (works for raw TCP, UDP, even non-HTTP protocols), and can sustain enormous packet rates because it never terminates the connection. Its ceiling is that it cannot make content-aware decisions — it doesn't know "/checkout" from "/health".

L7 terminates the TCP connection, parses the application protocol, and can route on host, path, method, or headers, plus do retries and circuit breaking. The cost is real: TLS termination, HTTP parsing, and buffering all consume CPU and add tens of microseconds to milliseconds of latency per hop.

Most large deployments use both: an L4 tier (AWS NLB, IPVS) absorbs raw connection volume and soaks up volumetric attack traffic cheaply, then hands surviving traffic to an L7 tier (Envoy/ALB/nginx) for the routing decisions that actually need content awareness.

L4L7NLBEnvoy

Round robin vs least-connections vs consistent hashing — pick one for a scenario

Round robin fits a pool of near-identical, stateless backends handling uniformly cheap requests — e.g., a fleet of static-content edge nodes. It's simple and requires no shared state, but it ignores that some requests (a report export) may take 100x longer than others (a health ping), so it can overload a backend that got unlucky with a run of heavy requests.

Least-connections fits exactly that uneven-cost scenario — an API gateway mixing cheap reads and expensive writes. It routes to whichever backend currently has the fewest in-flight requests, self-correcting for request-cost variance, at the price of needing shared or per-instance connection counters.

Consistent hashing fits a caching layer or sharded service where you want the same client (or key) to keep landing on the same backend — e.g., a video-transcoding cache where re-hitting the same node avoids re-fetching source assets. Adding or removing one backend only remaps ~1/N of the keyspace instead of reshuffling everything, unlike naive modulo hashing.

function pickBackend(key, ring):
    hash = consistentHash(key)          // e.g. SHA-1(key)
    node = ring.ceilingEntry(hash)      // first node clockwise of hash
    if node is null:
        node = ring.firstEntry()        // wrap around the ring
    if not isHealthy(node):
        node = ring.nextHealthy(node)   // skip unhealthy nodes clockwise
    return node.backend
Round robinLeast connectionsConsistent hashing

Connection draining during a rolling deploy

When a backend is about to be replaced, the control plane marks it "draining" instead of instantly removing it: the LB stops sending new connections to it immediately, but existing in-flight requests are allowed to finish, bounded by a drain timeout (commonly 30–300s depending on request length). Only after the timeout or when connection count hits zero is the instance actually terminated.

Skipping this step is the classic cause of deploy-time error spikes — killing an instance mid-request forces the client to retry (if idempotent) or simply fail. Draining decouples "stop giving me new work" from "shut down now."

Rolling deployGraceful shutdown

How do you load-balance the load balancer itself?

The LB tier can't have a single instance in front of it, or that instance becomes the new SPOF. Three common patterns solve this without another LB in front:

  • DNS/anycast: the same IP is announced from multiple physical locations; routers deliver each client to the topologically nearest, healthy instance.
  • ECMP: upstream routers hash flows across several LB instances that all answer for the same virtual IP, spreading load without any single instance being "the" entry point.
  • VRRP/keepalived: a floating virtual IP is owned by one active instance; on failure, a standby detects the missed heartbeat and takes over the VIP within a few seconds.

Cloud-managed options (AWS NLB, GCP's network load balancer) implement this pattern internally so the team never runs keepalived themselves — a strong default unless there's a reason to self-host.

ECMPAnycastVRRPkeepalived

The hot-backend problem: one instance is faster than the rest

If backends are heterogeneous (mixed instance types, one just finished a GC pause and is catching up, or one sits on faster hardware after a migration), naive round robin sends it the same share of traffic as a slower peer — either underutilizing the fast one or, worse, a "thundering herd" where least-connections floods the fastest backend the instant it looks least-loaded, then oscillates.

Fixes: weighted algorithms (assign capacity weights so a 2x backend gets 2x share), outlier detection (temporarily reduce traffic to instances with elevated latency/error rate, common in Envoy), and slow-start (a newly added or just-recovered backend receives ramped-up, not full, traffic for its first N seconds so it doesn't get slammed while still warming caches/JIT).

Weighted routingOutlier detectionSlow start

8. Summary

A load balancer is really two systems in one: a fast-path data plane that forwards packets/requests with minimal latency, and a slower control plane that keeps the data plane's view of "who is healthy" and "how to route" continuously up to date — while making sure the load balancer's own redundancy is never in question.

L4/L7 routing Health checking Consistent hashing Connection draining ECMP / VRRP Control plane
Interview tip When asked "design a load balancer," the strongest signal isn't naming HAProxy or Envoy — it's explicitly addressing that the load balancer itself must not be a single point of failure. Candidates who jump straight to algorithms and skip "how do you load-balance the load balancer" usually lose points on the non-functional requirements.
No comments
Leave a Comment