Load Balancing and Traffic Management Questions
Distributing requests across capacity: load-balancing algorithms (round-robin, least-connections, consistent hashing), L4 versus L7 balancing, health checks, and traffic shaping. Covers sticky sessions, canary and blue-green routing, rate limiting, and graceful draining. The traffic-distribution layer that keeps a scaled system balanced and available.
Compare active-active and active-passive architectures for making the load balancer layer itself highly available. Describe failure detection, how DNS and anycast fit into each approach, state-synchronization requirements, and the trade-offs in cost, complexity, and recovery time.
Sample Answer
Direct answer
Active-active runs multiple load balancers serving traffic simultaneously, giving near-instant failover since the survivors are already warm and already receiving traffic, at the cost of needing to handle state consistency across them. Active-passive keeps a standby idle until a failure is detected, which is operationally simpler and cheaper but has strictly worse recovery time, since the standby has to actually take over, not just absorb more of a load it was already serving.
Comparison
| Dimension | Active-active | Active-passive |
|---|---|---|
| Failure detection | Health checks and heartbeats among peers; a failed node's traffic share is redistributed to already-running peers | Heartbeat/cluster arbitration (e.g. VRRP, the Virtual Router Redundancy Protocol that lets a standby take over a shared virtual IP, or keepalived, a common Linux implementation of it) with quorum or fencing (forcibly cutting off a misbehaving node's power or network access so it truly can't serve) to avoid two nodes both believing they're primary (split-brain) |
| Role of DNS/Anycast | Anycast fits naturally: an identical IP announced from multiple active locations, the nearest healthy one wins at the routing layer | DNS failover (short TTL, single active record) is common; VRRP-style IP takeover is faster than DNS when both nodes share an L2 segment |
| State synchronization | Needs either stateless design or active replication (a shared session store, consistent hashing for locality), since more than one node is live at once | Simpler: only one node is authoritative at a time, replication to standby can be asynchronous since it isn't concurrently serving |
| Recovery time | Fast, often sub-second to a few seconds, since capacity is already running | Slower: bounded by detection time plus promotion time (VRRP takeover is quick, DNS-based failover is slow due to caching) |
| Cost | Higher: standby capacity isn't idle, it's paying its way as active capacity, but that means paying for redundancy at all times | Lower: standby sits idle until needed, but idle capacity is capacity you're paying for and not using day to day |
| Complexity | Higher: consistency, routing, and monitoring all have to account for concurrent writers/servers | Lower: one active path to reason about at a time; correct failover logic is the main thing that has to work |
When to choose which
Choose active-active when the recovery-time budget is tight (seconds, not minutes) and the service can either be made stateless at the LB layer or afford real-time state replication. Choose active-passive when the service's state is genuinely hard to keep consistent across concurrent writers, or when the cost of standing up full duplicate active capacity isn't justified by the actual availability requirement; a slightly slower failover is a legitimate trade-off, not a compromise, if the SLA allows for it.
Worked example
Take a 2-node pair using VRRP with a 1-second heartbeat and a 3-missed-heartbeat failure threshold. In active-passive, the standby only notices the active node is gone after 3×1s=3s of missed heartbeats, then needs roughly 1s more to take over the shared IP, so client-visible downtime is about 3+1=4s. In active-active with the same 3s detection window, the survivor is already serving traffic, so that window doesn't cause an outage, it just means the failed node's share has to shift over. If 10,000 RPS was split evenly across the pair (5,000 RPS each), the survivor's load roughly doubles to 10,000 RPS the moment the shift completes: active-active trades a ~4s outage for a ~4s capacity spike that the survivor must already be provisioned to absorb.
Trade-offs and pitfalls
- Split-brain is the active-passive failure mode that active-active sidesteps by construction, since there's no single "the" primary to disagree about; any active-passive design needs real fencing (forcibly cutting off a misbehaving node so it truly can't serve) or quorum, not just a heartbeat timeout, or a network partition can leave two nodes both serving as primary.
- Active-active's biggest hidden cost is usually not the LB layer itself but what's behind it: if the backends and session store aren't also built for concurrent multi-region writes, active-active load balancing in front of a single-writer database just moves the bottleneck, it doesn't remove it.
- DNS-based failover for active-passive is bounded by TTL in theory but by resolver and client caching behavior in practice; some clients and corporate resolvers ignore short TTLs, so don't treat a TTL setting as a guaranteed failover time.
- Testing failover regularly, not just designing for it, is what separates a working active-passive setup from one that fails silently the one time it's needed; an untested standby is a hypothesis, not a plan.
Why are load balancers used in distributed systems? Describe at least four distinct problems they solve, and how they contribute to horizontal scaling, availability, and fault isolation. Where would you typically place load balancers in a three-tier architecture (edge, internal service, service-to-service)?
Sample Answer
Direct answer
Load balancers exist to decouple how much traffic arrives from which single machine has to answer it. They solve at least four distinct problems: spreading load across available capacity so no single server bottlenecks, removing single points of failure through health-based failover, enabling safe rollouts such as canary (releasing a new version to a small slice of traffic first) and blue-green deployment (running old and new versions side by side, then cutting all traffic over at once) with graceful draining, and centralizing cross-cutting concerns like TLS termination and rate limiting. In a typical three-tier architecture, load balancers show up at the edge (public-facing traffic), between internal services (service to service), and inside a service mesh (sidecar to sidecar).
Structured elaboration
Named problems load balancers solve:
- Single-server bottleneck / horizontal scaling. Spreading requests across many instances lets the system add capacity by adding instances instead of a bigger single machine.
- Availability and automatic failover. Health checks remove failing instances from rotation so one bad node does not take down the service.
- Fault isolation and safe rollout. Canary and blue-green routing, plus connection draining during deploys, contain a bad release to a small slice of traffic instead of every user.
- TLS termination and centralized security policy. Certificates and security rules (WAF, Web Application Firewall: a layer that inspects and blocks malicious HTTP traffic before it reaches a backend, and rate limiting) live in one place instead of being duplicated on every backend.
- Rate limiting and overload protection. The balancer can shed or throttle excess traffic before it reaches backend capacity, protecting downstream services.
Placement in a three-tier architecture:
graph LR
Client[Client] --> EdgeLB["Edge LB (L7, TLS termination)"]
EdgeLB --> GwA[Gateway instance A]
EdgeLB --> GwB[Gateway instance B]
GwA --> InternalLB[Internal service LB]
GwB --> InternalLB
InternalLB --> SvcA[Service instance A]
InternalLB --> SvcB[Service instance B]
SvcA --> MeshLB["Sidecar LB (service-to-service)"]
SvcB --> MeshLB
MeshLB --> Downstream[Downstream service]
The edge tier handles public traffic and usually terminates TLS. The internal service tier balances across replicas of a given service, often behind a service discovery layer. The service-to-service tier (frequently a sidecar, a small proxy process deployed alongside each service instance, in a mesh) balances outbound calls one internal service makes to another, applying the same health-check and retry logic without a separate hop through a centralized balancer.
A fast way to spot gaps in an unfamiliar system's traffic layer: check whether all three tiers actually exist (a single edge LB fanning straight out to every backend is a common shortcut that skips the internal tier), whether health checks are active at each tier, and whether TLS termination and rate limiting are centralized or scattered across services.
Worked example
Three backend instances, each rated for 1,000 requests per second, pooled behind a load balancer: combined capacity is 3×1000=3000 requests per second. If health checks detect one instance failing and pull it out of rotation, capacity becomes 2×1000=2000 requests per second, so the system keeps serving at 2000/3000≈66.7% of its original capacity instead of dropping to zero, which is what a single unpooled instance would do on failure.
Trade-offs & pitfalls
- The load balancer itself becomes critical infrastructure. A single-instance balancer just moves the single point of failure up one layer; it needs its own high availability (active-active pair, DNS or anycast failover).
- Every additional tier (edge, internal, mesh) adds a proxy hop's worth of processing and another thing to operate; do not add a tier a system does not need.
- A misconfigured health check at any tier can silently remove that tier's benefit (failover, safe rollout) even though the balancer process is technically running.
Design a header-based routing framework at the edge load balancer that supports rules like 'route requests with header X-Canary=true to the canary pool' and 'route requests with a given X-Tenant-ID to a tenant-specific pool.' Cover rule storage and distribution, evaluation performance at the load balancer, conflict resolution between rules, and safe fallback behavior.
Sample Answer
Direct answer
Store rules as a versioned, prioritized ruleset in a central control plane, compile them into a fast local matcher (a trie, a tree structure where each path from the root spells out a prefix so a lookup finds a match in steps proportional to the key's length rather than scanning every rule, or decision graph, not a naive if-chain) on each edge node, and resolve conflicts with an explicit priority number plus a deterministic tie-break, never "whichever rule happens to match first" in file order. Safe fallback means every rule set has a default action if nothing matches or if validation fails, and that default is always the stable pool, never an unrouted request.
Structured elaboration
- Rule storage and distribution. Rules live in a control plane as immutable, versioned sets: predicate (header match, presence, regex, hash-bucket range), action (route to pool, set header), priority, and a version ID. The control plane pushes signed, compiled deltas to edge nodes (streaming or fast poll); each node keeps the last-known-good version and rolls back to it automatically if a new version fails local validation.
- Evaluation performance. Compile the ruleset into a structure suited to the predicate shape: a trie or hash lookup for exact-match headers like tenant ID, an Aho-Corasick automaton (a data structure that matches many string patterns against input in a single pass, faster than checking each pattern one at a time) or precompiled regex engine for pattern rules, and a short-circuit check for header presence before touching anything more expensive. The goal is that adding rules does not turn evaluation into a linear scan of every predicate on every request.
- Conflict resolution. Every rule carries an explicit priority number (lower evaluates first); ties break on rule creation time, then rule ID, so the outcome is fully deterministic and reproducible from the ruleset alone, not from evaluation order that happened to exist in memory.
- Header trust and spoofing. Only trust routing headers set by a component inside the trust boundary (the edge itself, after authentication), never a client-supplied header with the same name; if a header like X-Tenant-ID must originate from the client, validate it against the authenticated identity before using it for routing, or the routing layer becomes a way to reach another tenant's pool.
- Safe fallback. If no rule matches, or the active ruleset fails validation (schema error, missing referenced pool), route to the stable pool by default. This default must never be "no rule, no route."
graph LR
Author[Rule author: API/UI] --> RCP[Control plane: versioned ruleset]
RCP -->|signed compiled delta| Node1[Edge node: compiled matcher]
Req[Request] --> Node1
Node1 --> Match{Highest-priority match}
Match -->|tenant rule| TenantPool[Tenant-specific pool]
Match -->|canary rule| CanaryPool[Canary pool]
Match -->|no match / validation failed| Stable[Stable pool: default fallback]
Worked example
A simplified illustrative hash (real systems use SHA-256 or similar, not this): sum the ASCII codes of a key's characters and take the result mod 100. A canary rule routes to the canary pool when that value is below 10 (a 10% split).
| Key | ASCII sum | mod 100 | Below 10? | Routed to |
|---|---|---|---|---|
| "d" | 100 | 0 | yes | canary |
| "e" | 101 | 1 | yes | canary |
| "n" | 110 | 10 | no (boundary) | stable |
| "o" | 111 | 11 | no | stable |
The boundary case ("n", exactly 10) shows why the predicate needs an explicit, documented comparison (strictly less than, not less-than-or-equal): the threshold is a rule, not a fuzzy target, and the same key always lands on the same side of it, which is what lets a canary rollout stay stable across LB restarts without a shared session store.
Now layer conflict resolution: a request carries both a tenant header identifying "acme" (matches the tenant rule, priority 10) and a canary flag with a hash landing in the canary bucket (matches the canary rule, priority 20). Because lower priority numbers evaluate first, the tenant rule wins and the request goes to acme's dedicated pool, even though it would otherwise have qualified for canary.
Trade-offs & pitfalls
- Compiling to a trie or precompiled matcher costs build time on every rule update; for a control plane pushing frequent changes, measure compile time against your update frequency, not just steady-state lookup speed.
- Never let a client-controlled header carry routing authority without validating it against something the client cannot forge (an authenticated claim); an unvalidated tenant header is a direct tenant-isolation bypass, not just a routing bug.
- Rolling out a new ruleset version without a dry-run or shadow-evaluation stage means the first time you learn two rules conflict unexpectedly is in production; validate new versions against recent real traffic before activating them.
- Common wrong turn: treating "most specific rule wins" as if it were the same as explicit priority. Specificity is a heuristic humans use when writing rules; the engine needs an explicit, deterministic number, or two engineers' intuitions about "more specific" will eventually disagree.
You manage a pool of stateless web servers behind a single load balancer that sees bursty traffic and heterogeneous server capacity. Compare round robin, least connections, and weighted load balancing for this situation: how does each pick a server, and which would you choose?
Sample Answer
Direct answer
Round robin, least connections, and weighted load balancing differ in what information they use to pick a server. Round robin uses none, it just cycles through the pool. Least connections uses current load (active connection count) but assumes every server has equal capacity. Weighted load balancing (typically combined with one of the other two, giving weighted round robin or weighted least connections) uses declared server capacity. For a pool with bursty traffic and heterogeneous capacity, weighted least connections is the right default, because it is the only one of the three that reacts to both current load and known capacity differences.
How each one picks
| Algorithm | Selection rule | Reacts to load? | Reacts to capacity? |
|---|---|---|---|
| Round robin | Next server in a fixed rotation | No | No |
| Least connections | Server with fewest active connections | Yes | No (treats all servers as equal) |
| Weighted (least connections) | Server with the lowest active-connections-to-weight ratio | Yes | Yes |
Why plain round robin and plain least connections both fall short here
Round robin sends the same share of traffic to every server regardless of what it can handle, so a small server gets hit exactly as often as a large one and becomes the bottleneck under burst. Least connections is an improvement, since it avoids piling more work onto an already-busy server, but it still treats a 2-vCPU box and an 8-vCPU box as equally capable: an equal number of active connections is not an equal amount of work when the servers are different sizes.
Worked example: weighted least connections in action
Say the pool has three servers with declared capacity weights 4, 2, and 1 (roughly matching, say, an 8-vCPU, 4-vCPU, and 2-vCPU box). Weighted least connections picks, for each new request, whichever server minimizes:
scorei=wiciwhere ci is server i's current active connection count and wi is its weight. Ties are broken toward the higher-weight server. Tracing 7 back-to-back requests (a burst, so nothing completes mid-trace):
| Request | Scores (S1, S2, S3) | Picked | Active after |
|---|---|---|---|
| 1 | 0, 0, 0 (tie) | S1 | S1=1, S2=0, S3=0 |
| 2 | 0.25, 0, 0 (tie) | S2 | S1=1, S2=1, S3=0 |
| 3 | 0.25, 0.5, 0 | S3 | S1=1, S2=1, S3=1 |
| 4 | 0.25, 0.5, 1.0 | S1 | S1=2, S2=1, S3=1 |
| 5 | 0.5, 0.5 (tie), 1.0 | S1 | S1=3, S2=1, S3=1 |
| 6 | 0.75, 0.5, 1.0 | S2 | S1=3, S2=2, S3=1 |
| 7 | 0.75, 1.0, 1.0 | S1 | S1=4, S2=2, S3=1 |
After 7 requests the split is S1=4, S2=2, S3=1, exactly proportional to the declared weights of 4:2:1, even though every request arrived back-to-back with no completions in between. Plain round robin would have sent requests 1-7 as S1,S2,S3,S1,S2,S3,S1 (a 3:2:2 split), overloading S3 relative to its actual capacity.
Trade-offs and pitfalls
Weighted least connections needs two things plain least connections does not: accurate weights (usually derived from instance size or a load test, not guessed) and a mechanism to keep them current as capacity changes (autoscaling, degraded instances). It is also more stateful, the balancer has to track active connections per server accurately, which matters less for round robin. If request cost varies wildly and isn't reflected by connection count (a handful of expensive long-running requests versus many cheap ones), even weighted least connections can misjudge load, at which point request-cost-aware balancing or queuing becomes worth considering.
Design a high-throughput traffic mirroring pipeline that copies a sample of production requests to a staging cluster without adding latency to the production path. Cover asynchronous delivery, sampling strategy, masking of sensitive data, and how you would prevent a mirrored write from causing a real side effect in the staging system.
Sample Answer
Direct answer
Make the mirroring decision and enqueue non-blocking at the edge so production latency is unaffected, ship the sampled copy asynchronously through a durable stream to staging, and prevent side effects by treating the staging path as read-only by construction: either point it at a storage layer that discards or isolates writes, or convert write methods before they reach a real backend. Sensitive data gets masked before it leaves the edge, not after it lands in staging, since "after" means it already left the trust boundary.
Structured elaboration
- Decision point stays fast. At the edge or sidecar, evaluate sampling rules against cheap, already-available data (route, a few headers) and enqueue a copy into a local, bounded, in-memory buffer. The enqueue is fire-and-forget: on a full buffer, drop and increment a counter, never block the production request.
- Asynchronous transport. A background worker drains the local buffer into a durable, high-throughput stream (for example Kafka or Kinesis), batching and compressing for efficiency. This decouples production's request rate from staging's actual processing rate.
- Masking at the edge, not in staging. Redact or tokenize PII fields and strip auth secrets in the same process that decides to mirror, before the copy is serialized onto the stream. Waiting until staging to redact means an unredacted copy already crossed a trust boundary and sat in a durable log.
- Preventing write side effects. The staging ingress recognizes mirrored traffic (a shadow-mode header) and routes it through an adapter that either discards writes, redirects them to an isolated namespace or database, or serves reads from a replica while writes are stubbed. Add an idempotency key (a unique id attached to a request so the receiving system can recognize and discard a duplicate instead of applying it twice) to every mirrored request so retries in the pipeline itself cannot double-apply a write that did slip through.
- Fidelity validation. A comparator service consumes both the mirrored request and the staging response (correlated by an original trace ID carried in a header) and checks response-shape agreement (status code family, latency distribution, schema) against production, without needing a human to eyeball individual requests. This is what tells you the shadow environment is representative, not just quiet.
graph LR
Prod[Production request] --> Edge[Edge: sample + mask + async enqueue]
Edge --> Serve[Continue serving production, no wait]
Edge -.-> Buffer[Bounded local buffer]
Buffer --> Stream[Durable stream: Kafka/Kinesis]
Stream --> Consumer[Mirror consumer]
Consumer --> Ingress[Staging ingress: shadow-mode adapter]
Ingress --> Staging[Staging services, writes isolated]
Consumer --> Comparator[Comparator: prod vs staging fidelity]
Worked example
Production ingress runs at 50,000 rps. A 2% sampling target sends 50000×0.02=1000 rps into the mirroring pipeline. Staging is provisioned with 20% headroom over that mirrored rate: 1000×1.2=1200 rps of capacity.
If a single stream partition sustains 5,000 rps of small messages, one partition is technically enough for 1,000 rps (⌈1000/5000⌉=1), but the pipeline is provisioned with 4 partitions so 4 consumers can process in parallel; each partition then carries an average of 1000/4=250 rps, well under its 5,000 rps ceiling, leaving headroom for an uneven key distribution across partitions without any single consumer falling behind.
Trade-offs & pitfalls
- Fail-open is the only safe default: if the stream backs up or the buffer fills, drop mirrored traffic and keep counting drops, never apply backpressure (forcing an upstream sender to slow down or block because a downstream consumer can't keep up) to the production request path itself.
- Converting writes at the staging ingress (stubbing them out) keeps staging's data pristine but can make staging diverge functionally from production over time, since write-triggered side effects never happen there; using a read-only replica plus stubbed writes downstream keeps closer functional parity at the cost of extra infrastructure.
- Redacting at staging instead of at the edge is the single most common mistake in shadow-traffic designs: an unmasked copy sitting in a durable stream, even briefly, is a real data-exposure surface, not a theoretical one.
- Sampling and mirroring add real infrastructure cost (stream, consumers, staging capacity) proportional to sampled volume; treat the sampling rate as a cost dial, not just a load-control dial.
Unlock Full Question Bank
Get access to all Load Balancing and Traffic Management interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.