Vibe Engines
YouTube
System Design

Design a Load Balancer

Step 1 / 9

Learn system design by building a load balancer like NGINX, HAProxy or AWS ELB step by step.

The numbers to beatround-robineven rotationleast-connfollows real loadhashkey → server

The whole design, in writing

Learn system design by building a load balancer like NGINX, HAProxy or AWS ELB step by step. An interactive guide covering the single-server bottleneck, a pool of stateless servers behind one entry point, balancing algorithms (round-robin, least-connections, weighted, hashing), health checks that evict dead servers, L4 vs L7 routing, making the balancer itself redundant, session affinity vs statelessness, and the unhappy paths.

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What does a load balancer do?

A single server has a hard ceiling — a fixed number of requests per second before latency spikes and it falls over. Buy a bigger box and you've only raised the ceiling; you still have one machine that, when it dies, takes the whole service with it.

Clientsmany requests
New in this step: Clients.

A load balancer lets you run many servers behind one address. It spreads incoming requests across the pool so no single box is overwhelmed, and quietly routes around any server that fails. Clients see one endpoint; behind it, capacity and redundancy scale by adding boxes.

What the new pieces do

Clientsclient
A flood of users hitting one hostname. None of them should know or care which server actually answers.

Step 1 · The ceiling

One server, one ceiling

Everything runs on a single app server. As traffic climbs it saturates CPU and connections, latency balloons, and if it crashes the site is simply down. What's the real fix — a bigger server, or a different shape?

Clientsmany requests
The system as it stands at this step.

One server is saturating under load and is a single point of failure. Best fix?

  1. Vertical scaling raises the ceiling but it's still one machine with a hard limit — and still a single point of failure. When it dies, everything dies with it.

  2. Optimization buys headroom but not infinite headroom, and it does nothing for the single-point-of-failure problem. One box is one box.

  3. Horizontal scaling removes both problems at once: capacity grows by adding boxes, and one failure just means fewer boxes, not an outage — provided something spreads the traffic.

Scale out, not up: run many servers and share the load. That needs one component in front to decide, per request, which server handles it — and to notice when a server has died. That component is the load balancer.

Why this piece earns its place

Scaling out assumes the work actually divides, and here it does, because each request is independent of the others. What it does not divide is whatever those servers all sit in front of. If the saturated box was already blocked waiting on one database, one file system, one licence server, then ten copies of it block on the same thing and the graph barely moves — while that shared dependency now carries ten times the connections and may get worse. So before adding boxes I want to know what the old one was actually waiting on: CPU and connection limits are cured by more machines, contention behind them is not. That is also the honest limit of everything that follows. The pool is elastic; the tier behind it usually is not. Step 4 pushes session state into a shared store for good reasons, and that store then absorbs the traffic of the whole pool — it needs its own capacity plan, not an assumption that it scales because the thing in front of it does.

Step 2 · One door

A single entry point

Clients can't be told "connect to server 4 of 12" — the set of servers changes constantly as you scale and as boxes fail. They need one stable address. Who stands at that door?

Clientsmany requestsLoad Balancerentry point
New in this step: Load Balancer.

The server pool changes constantly. What do clients connect to?

  1. Client-side lists go stale the moment you add or lose a server, and push balancing logic into every client. You want one stable address that hides the pool.

  2. Clients hit one endpoint; the balancer holds the live list of servers and forwards each request to a healthy one. The pool can change freely behind it.

  3. DNS round-robin is crude balancing but can't health-check, reacts slowly (cached records keep sending traffic to dead boxes), and gives no per-request control. A real balancer does all three.

Put a load balancer at one address. Clients connect to it; it forwards each request to a chosen backend from a pool it manages. The pool can grow, shrink and lose members without a single client ever changing what it connects to.

Why this piece earns its place

One door only helps if what is behind it is accurate, and this step quietly assumes the balancer knows the pool. Somebody has to tell it. The cheap version is a static list in a config file, which is fine while servers are long-lived and someone edits that file on every change — and it fails in both directions you would expect: a new server sits idle because nobody added the line, and a decommissioned one keeps taking traffic until somebody remembers to delete it. Once instances come and go on their own, membership has to come from something that already knows — a service registry, an autoscaling group, the orchestrator’s endpoint list — and the balancer has to reload it without dropping live connections. Worth saying out loud: the health checks in step 5 are not a substitute for this. They decide whether a known server is serving right now; they cannot discover a server nobody told the balancer about.

What the new pieces do

Load Balancerbalancer
The single entry point. Picks a healthy backend for each request and forwards it — spreading load so no one server is overwhelmed.

Step 3 · Who gets it?

Which server should this request go to?

The balancer has a pool of servers and a request in hand. Send them all to server 1 and you've balanced nothing. Pick badly and one server melts while others idle. What's a good rule for choosing?

Load Balancerentry pointServer 1statelessServer 2statelessServer 3stateless
New in this step: Server 1, Server 2, Server 3.

A request arrives and the pool has 3 servers. How does the balancer choose one?

  1. That's not balancing — it hot-spots one box while the rest idle. You want to spread load, and ideally account for how busy each server already is.

  2. Random is a real strategy and evens out over time, but it ignores how loaded each server actually is, so a slow request can pile onto an already-busy box. Least-connections adapts to real load.

  3. Round-robin rotates evenly; least-connections favors the least-busy server (great when request cost varies); weighted respects unequal server sizes; hashing pins a key (like client IP) to a consistent server. Pick per workload.

Use a balancing algorithm. Round-robin rotates through servers evenly. Least-connections sends the next request to the least-busy server (best when request durations vary). Weighted variants give bigger boxes more traffic. Hashing (e.g. by client IP or URL) consistently maps a key to the same server.

Why this piece earns its place

Least-connections has one sharp edge worth naming, because it is the algorithm people reach for by default. It infers load from a count, and a server that is failing fast has the lowest count in the pool: it refuses or errors in a millisecond, its connections clear instantly, so it permanently looks like the idlest box and the balancer sends it more than anyone. The pool’s errors funnel into its sickest machine. The defence is not a different algorithm, it is pairing this one with the passive health checking of step 5, so a backend whose error rate spikes gets ejected instead of rewarded. The counter also has to measure the right thing. With keep-alive and HTTP/2 a single connection carries many requests, so connection count stops tracking work at all — count in-flight requests, or weight by observed latency, and treat the raw connection count as the proxy it is.

  • round-robineven rotation
  • least-connfollows real load
  • hashkey → server

What the new pieces do

Server 1service
One interchangeable app instance. Holds no session state, so any request can go to any server.
Server 2service
Another identical instance. Add or remove copies freely to scale with traffic.
Server 3service
The pool grows horizontally: more boxes = more capacity and more failure headroom.

Back of the envelope

round-robin / weighted
spread by count or capacity
least-connections
adapts to variable request cost
consistent hashing
sticky key → server, cache-friendly

Step 4 · Interchangeable boxes

Any server, any request

Least-connections and failover both assume any server can handle any request. But if server 3 holds your logged-in session in its local memory, routing your next request to server 5 logs you out. What has to be true for servers to be interchangeable?

ClientsLoad BalancerServer 1Server 2Server 3
The system as it stands at this step. · swipe to pan the diagram

For the balancer to route any request to any server, what must be true?

  1. Local session state pins a user to one server; if that server dies or the balancer routes elsewhere, the session is lost. State on the server breaks interchangeability.

  2. If servers hold no per-user state, any of them can serve any request. Move session and data into a shared store (Redis/DB) so the app tier is a pool of identical, disposable boxes.

  3. Broadcasting session to all servers is a consistency and bandwidth nightmare that gets worse as the pool grows. Centralize state instead of replicating it everywhere.

Make servers stateless: no per-user state on the box. Push session and application state into a shared store (a database or Redis). Now every server is identical and disposable — the balancer can route any request anywhere, add capacity by cloning, and lose a box without losing anyone's session.

Why this piece earns its place

Be precise about this in the room: stateless does not mean the state disappeared, it means it moved, and it moved into one shared thing that every server now depends on. You made the app tier disposable by making the store indispensable, so the store inherits the availability requirement the app tier just shed and has to be replicated for exactly the reason the servers are. There is a per-request cost too — what used to be a local memory read is now a network hop on the path of every request that touches a session. Two things follow. Keep what you put there small and hot: an identifier and a little context, not a serialized object graph. And notice that for pure identity you may not need the read at all — a signed token carried by the client proves who someone is with no lookup anywhere, at the price of not being able to revoke it the instant you want to. Server-side sessions cost a hop and buy instant revocation; tokens are the same trade run backwards.

Step 5 · Don't route to the dead

Health checks

A server can crash, hang, or get wedged while still accepting connections. If the balancer keeps sending it requests, users hit errors and timeouts. How does the balancer know a server is actually able to serve?

Health Checksprobe backendsServer 1statelessServer 2statelessServer 3stateless
New in this step: Health Checks.

A server crashes but the balancer doesn't know. How does it stop routing there?

  1. Manual removal is far too slow — thousands of requests hit the dead box first. Detection must be automatic and continuous.

  2. A single failure is noisy (a transient blip isn't a dead server). You want an active, repeated probe with a threshold, not a reaction to one bad request.

  3. A health check hits a lightweight endpoint (or opens a TCP connection) every second or two; after a few consecutive failures the server is pulled from the pool, and re-added once it passes again. Detection is automatic and bounded in time.

Run continuous health checks. The balancer probes each server on an interval — a TCP connect, or an HTTP GET /health. After N consecutive failures it evicts the server from the pool; when it passes again it's added back (ideally with slow-start so it isn't instantly overwhelmed). Requests only ever go to servers proven healthy right now.

Why this piece earns its place

Two knobs here, pulling against each other. The interval and the failure count set the detection window — a dead box keeps receiving traffic for roughly the interval times N, so at ~1–2s and a few consecutive failures that is seconds of errors — while tightening them toward zero means a garbage-collection pause or one slow probe flaps a healthy server out of the pool. Flapping is expensive: every eviction shifts that server’s share onto the survivors, which makes their probes slower, which evicts more of them. The second knob is what the probe asks. A /health that returns 200 straight out of the web framework proves the process is listening, not that it can serve — its connection pool to the store can be exhausted while the check stays green. A deeper check catches that but couples every server’s health to one shared dependency, so a store blip marks the whole pool dead at once. So put a floor under it: when every backend is failing the check, keep routing anyway. An empty pool is a guess that returns nothing to everybody.

  • ~1–2sprobe interval
  • N failsthen evict
  • slow-startramp recovered box

What the new pieces do

Health Checkshealth
Continuously probes each server. A failing box is pulled from the pool so no request is routed to a dead server.

Step 6 · Don't become the bottleneck

Who balances the balancer?

You removed the single point of failure from the app tier — and quietly created a new one. The load balancer is now the single box every request flows through. If it dies, everything behind it is unreachable. How do you make the balancer itself redundant?

Floating VIPone addressLoad Balanceractive + VIPStandby LBheartbeat
New in this step: Floating VIP, Standby LB.

The single load balancer is now the single point of failure. How do you fix it?

  1. "More reliable" still fails eventually, and when it does everything is down. You need a second balancer that can take over, not just a sturdier single one.

  2. Two balancers share a virtual IP via a heartbeat; if the active one dies, the standby claims the VIP in seconds and traffic continues. Clients never change the address they connect to.

  3. Client-side failover pushes complexity into every client and reacts slowly; many clients cache the dead address. Failover should be invisible, handled by the balancer pair and a shared VIP.

Run the balancer in a redundant pair sharing a floating virtual IP. A heartbeat keeps the standby watching the active one; if it stops responding, the standby claims the VIP within seconds and serving continues — clients keep hitting the same address. (At larger scale you go further: multiple balancers behind anycast or DNS, so there's no single active box at all.)

Why this piece earns its place

The failure this step introduces is the one the heartbeat cannot see. Losing the heartbeat means one of two things — the active balancer died, or the link between the pair died — and the standby cannot tell which, so the obvious rule, take the VIP when the heartbeat stops, can end with both balancers holding the same address and answering for it. That is split brain: connections land on whichever box the network learned most recently, and anything the pair keeps per-connection — sticky mappings, drain status, in-flight state — diverges between them. The fix is to not decide on a single signal: a second heartbeat over separate wiring, or a witness outside both boxes that arbitrates the claim, plus fencing so the loser releases the address before the winner takes it. The other thing I would raise is that a standby which only ever stands by is untested — its config drifts from the active one and you find out during the failure. Fail over deliberately, on a schedule, while someone is watching.

  • VIPshared address
  • heartbeatdetects failure
  • ~secondsto fail over

What the new pieces do

Floating VIProuting
A single virtual IP that the active balancer holds. On failover the standby claims it, so clients never change what they connect to.
Standby LBbalancer
A second balancer in lockstep. If the active one dies, it takes over the VIP within seconds.

Step 7 · When you can't be stateless

Sticky sessions & L4 vs L7

Sometimes a workload genuinely wants the same client on the same server (an in-progress upload, a local cache). And sometimes the balancer needs to route by content — path, host, cookie — not just spread connections. How much should the balancer understand?

Server 2statelessShared Storesession / data
New in this step: Shared Store.

Support session affinity ("sticky sessions") when needed — pin a client to a server via a cookie or consistent hash — but prefer stateless + shared store; stickiness re-introduces a little of the fragility you removed. And choose the layer: an L4 balancer routes by IP/port (fast, protocol-agnostic); an L7 balancer parses HTTP and can route by path/host/header, terminate TLS, and retry — more power, a little more cost.

Why this piece earns its place

Stickiness makes a promise it can only keep while the pool is stable, and how you implement it decides how badly it breaks. Pin clients with a plain hash over the list of servers and the mapping is a function of that list: evict one box — which step 5 will — and the list is shorter, so nearly every client rehashes somewhere new and the entire pool loses its affinity at the precise moment one server has already gone. That is what consistent hashing is for; only the failed server’s share moves and everybody else stays put. Cookie-based affinity survives a pool change instead, but has its own gap: a client that does not return the cookie is simply unpinned, so there has to be a sane answer for that request rather than an error. And since stickiness exists for the cases you could not make stateless, design the fallback so losing affinity costs a re-fetch or a re-upload, never a session.

What the new pieces do

Shared Storedata
Session and app state live here, not on the servers — which is exactly what lets any server handle any request.

Step 8 · The sharp edges

Failover storms & draining

Real failures cascade: a server dies and its load slams onto the survivors (possibly toppling them too — a failover storm); a recovered box gets flooded the instant it rejoins; a deploy kills servers mid-request.

ClientsFloating VIPLoad BalancerStandby LBHealth ChecksServer 1Server 2Server 3Shared Store
The system as it stands at this step. · swipe to pan the diagram

Keep headroom so survivors can absorb a failed peer's share. Slow-start a recovered or new server so it ramps up instead of being crushed on rejoin. Use connection draining (graceful shutdown): stop sending new requests to a server being removed but let in-flight ones finish. Cap retries with timeouts and circuit breakers so one slow backend can't stall the whole balancer.

Why this piece earns its place

Headroom is the one number this step actually asks you to pick, so here is how I would pick it. If a pool of n servers has to survive losing k of them, each survivor must carry its share while only n−k remain — which means the steady state runs at (n-k)/n of what the pool could take, and the difference is capacity you pay for every hour against an event that is rare. The arithmetic is why pool size changes the answer: surviving one loss out of a large pool costs a sliver of every box, while surviving one loss out of three costs a third of everything, permanently. Small pools are expensive to make resilient, which is a real argument for more and smaller servers rather than fewer big ones. Two cautions on measuring it. Size against the survivors’ knee, the load where latency turns upward, not average CPU, which looks calm right until it is not. And size for the correlated loss — a rack or an availability zone takes a slice of the pool at once, not one box.

You did it

You just designed a load balancer.

ClientsFloating VIPLoad BalancerStandby LBHealth ChecksServer 1Server 2Server 3Shared Store
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Scale out, not up — many servers survive failure and grow capacity linearly.
  • One stable entry point hides a fluid pool of backends from clients.
  • Balancing algorithms — round-robin, least-connections, weighted, hashing — pick the server per workload.
  • Stateless servers + a shared store make any box able to serve any request.
  • Health checks (active + passive) evict dead servers and slow-start recovered ones.
  • Make the balancer itself redundant — floating VIP + standby, or anycast/DNS at scale.
  • Sticky sessions and L4-vs-L7 for the cases that need them; drain, headroom and circuit breakers for failures.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Round-robin vs least-connections — when does each win?

    Round-robin simply rotates through servers and is ideal when requests are cheap and uniform, because then an equal count means equal load. It breaks down when request cost varies a lot: a server that happens to receive several slow requests keeps getting more even though it's overloaded. Least-connections routes to whichever server currently has the fewest in-flight requests, so it naturally shifts load away from a box that's bogged down by long requests. Use round-robin for uniform, short requests; least-connections for variable or long-lived ones (streaming, uploads, slow queries).

  2. L4 vs L7 load balancing — what's the trade-off?

    An L4 balancer operates at the transport layer, forwarding TCP/UDP connections by IP and port without ever looking inside the request. That makes it extremely fast and protocol-agnostic, but it can't make decisions based on the request content. An L7 balancer terminates the connection, parses HTTP, and can route by path/host/header/cookie, terminate TLS, rewrite headers, and retry failed requests intelligently — at the cost of more CPU per request and more moving parts. A common pattern is L4 at the very edge for raw throughput and DDoS absorption, with L7 balancers behind it for smart application routing.

  3. Why must servers be stateless, and where does session state go?

    The balancer's power — routing any request to any server, adding/removing capacity, surviving a server death — all depends on servers being interchangeable. The instant a server holds something a user needs (their session, an in-progress cart) in local memory, that user is pinned to that box and a failover loses their state. The fix is to move state out of the app tier into a shared, durable store (a database, or Redis for sessions), so the app servers become a pool of identical, disposable boxes. When you genuinely can't (a stateful upload), you fall back to sticky sessions, accepting that you've traded away some of the resilience.

  4. How do you keep the load balancer from being the single point of failure?

    You make the balancer tier redundant, the same way you made the app tier redundant. The classic pattern is an active-standby pair sharing a floating virtual IP: a heartbeat protocol lets the standby detect the active one's death and claim the VIP within seconds, so clients keep connecting to the same address throughout. At larger scale you go further and run many balancers that all advertise the same address via anycast (or that DNS spreads clients across), so there is no single active balancer to lose — the balancer tier becomes a pool just like the servers behind it.

  5. What is a failover storm and how do you prevent it?

    When a server dies, the traffic it was handling doesn't disappear — it redistributes onto the surviving servers. If the pool was already running near capacity, that extra load can push a survivor over the edge, which redistributes again onto an even smaller pool, cascading until the whole tier collapses. Prevention is about headroom and pacing: run the pool with enough spare capacity that it can absorb the loss of one (or a few) servers; slow-start recovered and new servers so they ramp up instead of being instantly flooded; and use timeouts plus circuit breakers so a slow or failing backend is shed quickly rather than tying up connections and dragging the balancer down with it.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. A load balancer lets you…

    It spreads requests across a pool and evicts dead servers, enabling scale-out and redundancy.

  2. Least-connections balancing is preferable to round-robin when…

    Least-connections follows real load, so a server stuck with slow requests stops getting more.

  3. Servers must be stateless so that…

    State in a shared store makes app boxes interchangeable and disposable.

  4. Health checks exist to…

    Active + passive probes evict a dead server within a couple of intervals and restore it on recovery.

  5. The load balancer itself avoids being a single point of failure by…

    A standby claims the VIP on heartbeat loss in seconds; anycast/DNS pools it further at scale.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Spread load: run many servers behind one address and share requests across the pool.
  • One door: a single stable entry point hides a fluid set of backends from clients.
  • Choose a server: round-robin, least-connections, weighted, or hashing per request.
  • Evict the dead: health-check backends and route only to servers proven healthy right now.
  • Stay up: make the balancer itself redundant, and drain / L4-vs-L7 for the cases that need them.

The qualities that shape everything

Each one names the mechanism that buys it.

Survive a server death and grow capacity
Scale out — run many stateless servers behind the balancer so one failure means fewer boxes, not an outage, and capacity grows by adding boxes.
One stable address as the pool churns
A single entry point is a level of indirection: clients hit one endpoint while the balancer holds the live, changing list of backends.
Spread load well for the workload
A balancing algorithm — round-robin, least-connections, weighted, or consistent hashing — chosen to the request shape.
Any server can serve any request
Stateless servers with session/state in a shared store make boxes interchangeable and disposable, so failover loses no session.
Never route to a crashed or hung server
Continuous active + passive health checks evict a server after N failed probes and slow-start it back on recovery.
The balancer isn’t itself a single point of failure
A redundant active-standby pair shares a floating VIP over a heartbeat, so the standby claims the address in seconds (anycast/DNS at larger scale).

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Scale out (many servers) over scaling up (a bigger box)

A bigger machine still has a hard ceiling and is still one single point of failure; many servers grow capacity linearly and make one failure survivable.

A load balancer at one address over round-robin DNS listing every server

DNS round-robin can’t health-check and reacts slowly — cached records keep sending traffic to dead boxes; a real balancer holds the live list and gives per-request control.

Least-connections (and friends) over random server selection

Random evens out over time but ignores how loaded each server already is, so a slow request piles onto a busy box; least-connections follows real load, weighted respects size, hashing pins a key.

Stateless servers + shared store over per-server local session state

Local session pins a user to one box, so a failover logs them out; moving state into a shared store makes every server identical and a lost box just costs a retry.

A standby + floating VIP over just a more reliable single balancer

"More reliable" still fails eventually and takes everything down with it; a second balancer sharing a VIP claims it on heartbeat loss so clients never change the address.

The answer, out loud

What a strong answer to “Design a Load Balancer” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    What this thing has to do

    I want to be clear about what we’re designing before I draw anything. We have a service that has outgrown one machine, and the job is to run many machines behind one address so traffic spreads across them and losing one of them isn’t an outage. So the requirements I’d write down are: spread load across a pool, keep one stable entry point, choose a server per request, stop sending work to servers that can’t serve it, and make sure the thing doing all of that isn’t itself the thing that takes us down.

  2. 3–8 min

    Up or out

    The first decision is up or out. A bigger box only buys a higher ceiling — it’s still one machine, and the day it dies the service dies with it. Scaling out fixes both problems at once: capacity grows by adding boxes, and a failure costs a fraction of the pool instead of all of it. What that needs is something in front deciding, per request, which server handles it, and noticing when one has stopped working. That component is the balancer, and the rest of this is me building it.

    Built in step 1: One server, one ceiling
  3. 8–13 min

    One address in front of a changing pool

    That component sits at one address. Clients connect to it, it forwards to a backend it picked, and the pool can grow, shrink and lose members without a single client being reconfigured. I don’t want clients holding a list of servers, because that list is stale the moment anything changes. DNS with several records is the other shortcut people reach for; it spreads traffic roughly, but it can’t tell a live server from a dead one, and cached records keep pointing at boxes that are already gone.

    Built in step 2: A single entry point
  4. 13–19 min

    Choosing a server

    Then, which server. Round-robin just rotates, and that’s the right answer when requests are cheap and look alike, because an equal count really is equal load. Once request cost varies that stops being true, and I’d move to least-connections, which sends the next request wherever the fewest are currently in flight, so a box bogged down with slow work stops being handed more. Weighted variants cover a pool where the boxes aren’t the same size. And hashing on something like client IP or the URL puts the same key on the same server every time, which I’d reach for when I want cache locality rather than an even spread.

    Built in step 3: Which server should this request go to?
  5. 19–25 min

    Why the servers have to be empty

    All of that assumes any server can take any request, and that’s only true if the servers hold nothing of their own. If a user’s session lives in one box’s memory, routing them anywhere else logs them out, and losing that box loses their session. So I’d keep the app tier stateless and put session and application state in a shared store. That’s what makes the boxes genuinely interchangeable: I can clone one to add capacity, kill one and pay a retry, and the routing decision stays free to send anything anywhere.

    Built in step 4: Any server, any request
  6. 25–31 min

    Not routing to the dead

    Next, never route to a server that can’t serve. The balancer probes each backend on an interval and pulls it out of the pool after a few consecutive failures, then adds it back once it passes again — and I’d ramp a returning server up rather than restore its full share instantly. Alongside that I’d watch real traffic for errors and timeouts, because that reacts at the speed of the actual failure, while the active probe is what I trust to confirm a recovery. Detection has to be automatic and bounded in time.

    Built in step 5: Health checks
  7. 31–37 min

    Who balances the balancer

    Here I’d call out that I’ve moved the single point of failure rather than removed it — every request now flows through one balancer. So I’d run a pair sharing a floating virtual IP with a heartbeat between them: if the active one stops answering, the standby claims the address within seconds and clients never change what they connect to. At larger scale I’d rather not have one active box at all, and I’d put several balancers behind anycast or DNS so the balancer tier is a pool in the same way the servers behind it are.

    Built in step 6: Who balances the balancer?
  8. 37–41 min

    The cases that don’t fit

    Two things I’d flag before closing. Sometimes you genuinely can’t be stateless — an upload in progress, a warm local cache — and then I’d pin that client to a server with a cookie or a hash, while being honest that I’ve traded back some of the resilience I just built. And there’s the layer question: routing by IP and port is fast and knows nothing about the request, whereas parsing HTTP lets me route by path or host, terminate TLS, and retry intelligently, for more work per request. Many stacks run both: fast at the edge, smart behind it.

    Built in step 7: Sticky sessions & L4 vs L7
  9. 41–45 min

    The trade-off, and what’s left

    So the shape is a pool of stateless servers, one balancer in front choosing by a policy that fits the workload, health checks deciding who’s eligible, and a redundant pair so the balancer isn’t the new weak point. The trade-off running through all of it: every piece of intelligence I put in that middle box — smarter routing, stickiness, TLS, retries — buys behaviour and costs latency and one more thing that can fail. With more time I’d work the unhappy paths properly: how much spare capacity the pool carries, draining connections so a deploy doesn’t drop requests mid-flight, and timeouts and circuit breakers so one slow backend can’t stall the balancer.

What this teaches

Learn system design by building a load balancer like NGINX, HAProxy or AWS ELB step by step. An interactive guide covering the single-server bottleneck, a pool of stateless servers behind one entry point, balancing algorithms (round-robin, least-connections, weighted, hashing), health checks that evict dead servers, L4 vs L7 routing, making the balancer itself redundant, session affinity vs statelessness, and the unhappy paths.

Key takeaways

  • Scale out, not up — many servers survive failure and grow capacity linearly.
  • One stable entry point hides a fluid pool of backends from clients.
  • Balancing algorithms — round-robin, least-connections, weighted, hashing — pick the server per workload.
  • Stateless servers + a shared store make any box able to serve any request.
  • Health checks (active + passive) evict dead servers and slow-start recovered ones.
  • Make the balancer itself redundant — floating VIP + standby, or anycast/DNS at scale.
  • Sticky sessions and L4-vs-L7 for the cases that need them; drain, headroom and circuit breakers for failures.

Concepts covered

  • What does a load balancer do?
  • One server, one ceiling
  • A single entry point
  • Which server should this request go to?
  • Any server, any request
  • Health checks
  • Who balances the balancer?
  • Sticky sessions & L4 vs L7
  • Failover storms & draining
built to never be the bottleneck it removes — make the calls, kill a server, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs