Guide · System design
Load Balancers Explained: L4 vs L7, Global vs Local
30 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- A load balancer sits in front of a pool, a group of servers doing the same job, and passes each request to one of them. It skips servers that fail its health checks, small test requests sent on a timer.
- An L4 load balancer picks a server for each connection using only addresses and ports, without reading what is sent. An L7 load balancer reads each web (HTTP) request, so it can route by the URL path or by a cookie, a small value the browser sends back.
- The algorithm is the rule that picks the server. Round robin takes turns, least connections picks the server with the fewest requests in progress, and hashing keeps one user or key on one server.
- Sticky sessions pin a user to one server, but keeping session data in a shared database every server can read is safer. Global load balancing picks a region, often through DNS, the system that turns names into addresses. A local load balancer then picks a server there.
- Beginners can stop after the single point of failure section. Engineers then get what we measured on nginx 1.24 and HAProxy 2.8, two common open-source load balancers. A frozen server left the HAProxy pool in about 14 to 24 seconds. With default settings, nginx kept sending users to a server that answered every request with an error (HTTP 500). It also did not retry a timed-out POST, a request that changes data such as holding a seat.
What goes wrong when a busy site has no load balancer?
One broken server fails a share of all users, because nothing in front of the servers notices it and steers traffic away. Spotting and skipping that server is the first job of a load balancer.
Picture a common setup. A ticketing site sells concert and comedy tickets from four app servers, called app-1 to app-4. Its domain name points straight at all four addresses through DNS, the internet's phone book that turns names into addresses. The setup is made up, but the failure is real.
Tickets for a popular comedian's tour open at noon on a Friday. About 60,000 people arrive in the first ten minutes. At 12:04, app-3 runs out of memory and stops answering, but its address stays in DNS.
- About one buyer in four keeps reaching app-3 and sees a spinning page, then an error.
- Phones that saved app-3's address keep trying it for minutes, even after the on-call engineer removes it from DNS.
- Buyers who refresh onto another server lose the seats they held, because seat holds live in each server's own memory.
- app-1 sits at 30 percent CPU while app-2 runs at 95 percent, because DNS split users unevenly.
The team's fix has four parts, and they match the next sections. Put a load balancer in front, pick a sensible algorithm, add health checks, and move seat holds out of server memory.
What is a load balancer and why do you need one?
A load balancer is a server or managed service that receives every request for your site and passes each one to one of several backend servers. It spreads the work and skips servers that are down.
Users now talk to one address: the load balancer's. Behind it sits the pool, the group of backend servers doing the real work; AWS calls each one a target. Users never see app-3's own address, so a broken app-3 can be hidden quickly.
Think of a busy petrol pump on the Pune to Mumbai highway. An attendant at the entrance waves each vehicle to a free nozzle. Drivers do not pick a nozzle themselves, so no queue forms at one while others sit idle.
- The attendant is the load balancer, and the nozzles are the servers in the pool.
- Choosing which nozzle gets the next car is the algorithm.
- Spotting a nozzle that shows an error, and skipping it, is the health check.
Where the picture breaks: the attendant sees every nozzle directly. A load balancer only learns that a server is sick from probes and failed requests, so it always finds out a little late.
A load balancer gives the site three things.
- Spreading load. Work is shared, so 60,000 buyers become about 15,000 per server instead of a pile on one.
- Hiding failures. A dead server is skipped once health checks catch it, from about 15 seconds to a minute depending on settings. Nobody waits for phones to forget an old address.
- Easy changes. You can add, remove or update servers behind the single address while users notice nothing.
Here is the site after the team's first change. Notice that the shared session store sits beside the servers, not inside any one of them.
A ticketing site with a load balancer in front
Drag to exploreBuyers reach one address. Notice app-3 still gets health checks but no buyers, and seat holds live in the shared store, not in any server.
Diagram connections:
- Buyers on phone and web to Load balancer (Every request)
- Load balancer to app-1
- Load balancer to app-2
- Load balancer to app-3 (Health checks only)
- Load balancer to app-4
- app-1 to Shared session store
- app-2 to Shared session store
- app-4 to Shared session store
- Buyers on phone and web: About 60,000 in the first ten minutes
- Load balancer: One public address for the whole site
- app-1: Healthy, in the pool
- app-2: Healthy, in the pool
- app-3: Failed its checks, out of the pool
- app-4: Healthy, in the pool
- Shared session store: Holds every buyer's seat holds
What is the difference between L4 and L7 load balancers?
An L4 load balancer routes each connection using only network details, like IP addresses and ports, without reading the request. An L7 load balancer reads each HTTP request and can route on its path, headers or cookies.
The numbers come from the OSI model, a seven-layer way of describing networks. Layer 4 is the transport layer, where TCP and UDP carry data between two machines. Layer 7 is the application layer, where HTTP, the language of browsers and web servers, lives.
L4: fast, and blind to the request
An L4 balancer sees a new TCP connection arrive and picks a server for it. Every packet, a small chunk of data, in that connection then goes to the same server. It never opens the envelope, so it cannot tell a seat booking from an image download.
AWS documents this for its Network Load Balancer. It picks a target with a flow hash. A hash is a function that turns an input into a number, always the same number for the same input.
Here the input is the protocol, both addresses and ports, and the TCP sequence number, a byte counter that starts at a different value for each connection. So each connection stays with one target for its whole life, but two connections from the same phone can land on different targets.
Because so little work happens per packet, L4 is fast and can carry TCP traffic and, in many products, UDP traffic, not just web pages. L4 balancers come in two builds. Some forward packets without ending the connection: Google's Maglev paper reports one machine filling a 10 Gbps (10 gigabits a second) link with small packets.
Others, like HAProxy in TCP mode, end the client's connection and open their own to the server. We checked on HAProxy 2.8.16: the server saw a different source port from the client's, yet received the bytes unchanged. Neither build reads the request.
L7: more work, but it reads the request
An L7 balancer ends the user's connection, reads the HTTP request, then sends it to a backend over its own connection. That is why it is also called a reverse proxy: a server that receives requests on behalf of other servers.
Reading the request lets it make choices L4 cannot. It can send seat booking requests to booking servers and search requests to search servers. It can read cookies, add headers, retry a failed request elsewhere, and handle HTTPS, the encrypted form of HTTP, in one place.
AWS's Application Load Balancer works this way. It checks listener rules, the routing rules you write, in priority order from the lowest number. The first match names a target group, which is AWS's word for a pool, and that group's algorithm picks a server.
It also adds an X-Forwarded-For header, an extra labelled line on the request, so the backend learns the buyer's real address. Without it, every request would seem to come from the load balancer.
| L4 load balancer | L7 load balancer | |
|---|---|---|
| What it reads | IP addresses, ports and protocol | The whole HTTP request: path, headers, cookies |
| What it balances | Whole connections | Single requests |
| Work per request | Little: it never parses HTTP | More: it parses HTTP and often decrypts HTTPS |
| Traffic it carries | TCP and, in many products, UDP, such as game or database traffic | HTTP, HTTPS, gRPC (service-to-service calls over HTTP/2) and WebSockets (connections that stay open) |
| HTTPS | Can pass encrypted traffic straight through | Usually decrypts it to read the request |
| Routing by path or cookie | Not possible | Yes |
| Job at the ticketing site | Takes every connection from the internet | Splits seats, search and event pages into pools |
The team uses both, which is a common pattern. A fast L4 layer takes every connection from the internet, and an L7 pair behind it reads each request and picks a pool. Follow one request below and watch where the path starts to matter.
How a ticket request passes through L4 and L7
Drag to exploreNotice that the L4 box moves the connection without reading it. Only the L7 box reads the path, so only it can choose the pool.
Diagram connections:
- Buyer's phone to L4 load balancer (HTTPS connection)
- L4 load balancer to L7 load balancer pair (Bytes passed on, unread)
- L7 load balancer pair to Seat booking pool (Path starts with /seats)
- L7 load balancer pair to Search pool (Path starts with /search)
- L7 load balancer pair to Event pages pool (Everything else)
- Buyer's phone: Opens an HTTPS connection
- L4 load balancer: Spreads TCP connections, reads no HTTP
- L7 load balancer pair: Ends HTTPS, reads path and cookies
- Seat booking pool: 6 servers, least connections
- Search pool: 4 servers, round robin
- Event pages pool: 3 servers, round robin
Which load balancing algorithm should you use?
Use round robin when servers are equal and requests take similar time, and least connections when request times vary a lot. Use a hash only when the same user or key must keep landing on the same server.
The algorithm is the rule that picks a server for each new request or connection.
Round robin and weighted round robin
Round robin hands out requests in turn: app-1, app-2, app-3, app-4, then back to app-1. It is the default in nginx, a widely used open-source web server and proxy, and in AWS's Application Load Balancer. It is fair when every request costs about the same.
Weighted round robin gives bigger servers more turns. The nginx docs give one server a weight of 3 and two others a weight of 1, so every five requests split three, one and one. On nginx 1.24.0 we saw app-1, app-2, app-1, app-3, app-1, repeating.
Least connections
Least connections sends each new request to the server with the fewest requests in progress right now. AWS's version is called least outstanding requests. AWS says it is commonly used when requests vary in complexity or targets vary in processing capability.
That fits the ticketing site. Loading an event page takes about 20 milliseconds, but holding seats takes about 400, because it locks rows in the database. Round robin counts both as one turn, so a server that gets several seat holds in a row falls behind.
This is the petrol pump attendant at his best. He does not wave vehicles to nozzles in strict turn. He watches which nozzle is actually free, because a bike fills in one minute and a loaded truck takes five.
Power of two choices
Checking every server's count on every request gets costly with hundreds of servers. So some proxies pick two servers at random and send the request to the less busy one. This trick is called the power of two choices.
Envoy, an open-source proxy, does this in its least request balancer when servers have equal weights. nginx offers it as the random two method. Envoy's docs cite research showing it is nearly as good as checking every server.
Hashing
A hash method turns something about the request, like the user's IP address or a cookie, into a number that picks the server. The same input lands on the same server as long as the pool does not change. nginx's ip_hash method works this way.
Plain hashing has a catch. It takes the hash modulo the number of servers, which means the remainder after dividing, so changing that number moves most users. Consistent hashing places servers and users on a ring, so only a small share moves.
We tested both with 100,000 made-up user IDs. When app-3 left, plain hashing moved 75 percent of users to a new server. A consistent-hash ring moved 24.6 percent, which is just app-3's own users.
Here is the heart of a least-busy picker in Python. Each server object has an in_pool flag, set by health checks, and an active count of requests in progress.
import random
def pick_server(servers):
pool = [s for s in servers if s.in_pool]
if not pool: # nothing healthy: fail open
pool = servers
if len(pool) == 1:
return pool[0]
a, b = random.sample(pool, 2) # power of two choices
return a if a.active <= b.active else bWhat just happened? random.sample draws two different servers, and the last line keeps the one with fewer active requests. The fallback near the top sends traffic to every server when none look healthy, which the next section explains.
We ran it 30,000 times with app-3 out of the pool and active counts of 2, 9 and 5. It chose app-1 about two times in three and app-4 one time in three. It never chose the busiest server, app-2, because that one loses every pair it is drawn into.
How do load balancer health checks work?
A health check is a small test request the load balancer sends to each server on a timer. If a server fails a set number of checks in a row, it leaves the pool. When it passes enough in a row, it comes back.
These are active health checks, because the load balancer goes looking. AWS's Application Load Balancer sends an HTTP GET, a request that only reads data, every 30 seconds by default for instance and IP targets. It waits 5 seconds for a reply and expects status code 200, which means OK.
Two thresholds stop flapping, which means servers bouncing in and out of the pool on every small hiccup. By default, AWS marks a target unhealthy after 2 failures in a row. It needs 5 passes in a row to mark it healthy again.
Passive health checks watch real traffic instead. nginx counts failed attempts to reach a server within a window called fail_timeout, 10 seconds by default. With the default max_fails of 1, a single failure takes the server out for that window.
Follow one probe from the timer to its end. The team's checker probes every 10 seconds, and a server never leaves or returns on a single result.
One health check probe, step by step
Drag to exploreNotice that each result resets the other counter. Two failures in a row remove a server; three passes in a row bring it back.
Diagram connections:
- Probe every 10 seconds to Healthy reply within 2 s?
- Healthy reply within 2 s? to Count a pass (Yes)
- Healthy reply within 2 s? to Count a failure (No, or too slow)
- Count a pass to 3 passes in a row?
- Count a failure to 2 failures in a row?
- 3 passes in a row? to Put server back in pool (Yes)
- 2 failures in a row? to Take server out of pool (Yes)
- Probe every 10 seconds: GET /healthz to each server
- Healthy reply within 2 s?: Status 200, and fast enough
- Count a pass: Failure count goes back to zero
- Count a failure: Pass count goes back to zero
- 3 passes in a row?: Asked only for a server outside the pool
- 2 failures in a row?: Asked only for a server inside the pool
- Put server back in pool: Starts getting buyers again
- Take server out of pool: Still probed, gets no buyers
The team's checker keeps two counters per server, following the diagram.
FALL = 2 # failures in a row before a server leaves the pool
RISE = 3 # passes in a row before it comes back
def record_probe(server, passed):
if passed:
server.passes += 1
server.fails = 0
if not server.in_pool and server.passes >= RISE:
server.in_pool = True
else:
server.fails += 1
server.passes = 0
if server.in_pool and server.fails >= FALL:
server.in_pool = FalseThe key detail is that each result resets the opposite counter. So fails always means failures in a row, not failures ever. If a probe starts every 10 seconds with a 2-second timeout, a frozen server leaves the pool 12 to 22 seconds after it freezes.
The spread depends on where the probe timer was when the server froze. On HAProxy with the same settings we measured about 14 to 24 seconds; the engineer section explains the gap.
What should a health check test?
A good health endpoint quickly checks what the server needs to sell a ticket. The team's endpoint, a small page called /healthz, confirms the app can reach the session store and the database within 200 milliseconds.
Do not make it too deep, though. Suppose every server's check also called the payment provider, and that provider slowed down. Every server would fail at once, and the load balancer would empty the pool over a problem it cannot fix.
Draining and slow start
Health checks handle surprise failures. For planned removals, like a deploy, use draining: the balancer stops sending new requests to a server but lets requests in progress finish. AWS calls this the deregistration delay and waits up to 300 seconds by default, finishing early once a server has no requests or connections left.
The reverse is slow start. By default, AWS gives a new target its full share as soon as it passes one health check. Slow start mode instead raises its share linearly, so caches and database connections can warm up. In nginx, slow_start is part of the paid NGINX Plus subscription.
What are sticky sessions and when should you avoid them?
Sticky sessions, also called session affinity, make the load balancer send all of one user's requests to the same server. Avoid them when you can, by keeping session data in a shared store instead.
The ticketing site leaned on stickiness by accident. Each server kept seat holds in its own memory, so a buyer moved to another server lost the seats. Stickiness hid that problem until app-3 died.
There are two common ways to stick. You can hash the user's IP address, as nginx's ip_hash does. Or you can use a cookie, a small piece of data the browser sends back with every request, that names the server.
AWS's Application Load Balancer can make its own cookie, called AWSALB, with a stickiness duration you set from 1 second to 7 days. If that cookie points to a target that is unhealthy or deregistered, the balancer picks a new target and updates the cookie.
- Uneven load. A server that picked up many long sessions stays busy, while a newly added server gets little traffic until old cookies expire.
- Lost state. When the sticky server dies, its memory dies with it, so the user loses the session anyway.
- Shared addresses. nginx's ip_hash uses only the first three parts of an IPv4 address, the common four-number form such as 127.0.0.1. In our test, clients 127.0.0.1, 127.0.0.2 and 127.0.0.200 all landed on app-1.
So a whole office or mobile network sharing one address range can pile onto one server. The team's fix was to move seat holds into a shared session store, a fast in-memory database every server reads. Now any server can handle any request.
That makes the app servers stateless, meaning they keep nothing a user needs between requests, and stickiness can be switched off.
What is the difference between global and local load balancing?
Global load balancing picks which region or data centre a user goes to. Local load balancing picks which server inside that region handles the request.
The team's next goal is a second region, with servers in Mumbai and Hyderabad, so one region's outage cannot stop a sale. A local load balancer cannot help with that, because it lives inside one region.
Global balancing is often done with DNS. When a phone asks for the site's address, the DNS service answers by rules instead of giving one fixed answer. Amazon Route 53, for example, offers latency, geolocation, weighted and failover routing policies, among others.
- Latency routing sends users to the region that gives them the best latency, which is how long a request takes to get there and back.
- Geolocation routing sends users by where they are, such as every user in one country to one region.
- Weighted routing sends set shares of users to each place, such as 90 percent to one region and 10 percent to another.
- Failover routing sets up active-passive failover: users go to a backup region when the main one is unhealthy.
DNS has the same weakness that hurt the ticketing site. Resolvers, the DNS servers that look up names for your phone, may cache each answer for up to its TTL, the record's time to live. So a change reaches users only as those copies expire.
Here is the team's two-region plan. Look for the two layers: one global decision at the top, then a normal local setup inside each region.
Global and local load balancing for the site
Drag to exploreLook for two separate decisions: DNS picks the region, then a local load balancer inside that region picks the server.
Diagram connections:
- Buyers across India to Global DNS routing (Where is the ticket site?)
- Global DNS routing to Mumbai load balancer (Faster for this buyer)
- Global DNS routing to Hyderabad load balancer (Faster, or Mumbai down)
- Mumbai load balancer to Mumbai app servers
- Hyderabad load balancer to Hyderabad app servers
- Mumbai app servers to Bookings database
- Hyderabad app servers to Bookings database (Writes go to Mumbai)
- Buyers across India: Phones and browsers
- Global DNS routing: Picks a region by latency and health
- Mumbai load balancer: Local layer for the main region
- Hyderabad load balancer: Local layer for the second region
- Mumbai app servers: Least connections inside the region
- Hyderabad app servers: Least connections inside the region
- Bookings database: Main copy in Mumbai, a copy in Hyderabad
How do you stop a load balancer becoming a single point of failure?
You run more than one load balancer and give them a way to share or take over the public address. A single load balancer in front of four servers only moves the weak spot from the servers to itself.
- Active-passive pair. Two balancers share a floating IP address that can move between machines, often using VRRP, a standard takeover protocol. With its default 1-second announcements, the standby takes over after about 3 to 4 silent seconds.
- Active-active behind routers. Routers spread packets across several balancers using ECMP, equal-cost multipath routing. When one fails, routers may move some other connections too, so the balancers need consistent hashing to keep most connections on their servers.
- Managed cloud balancers. AWS creates a load balancer node in each Availability Zone you enable, a zone being a separate group of data centres inside one region.
- Several addresses in DNS. Route 53's multivalue answer policy returns up to eight healthy records at random, so clients have somewhere else to try.
Maglev uses the active-active pattern, with routers spreading packets over many Maglev machines. Its paper says consistent hashing and connection tracking, which remembers where each connection went, limit the damage when machines fail.
Three weeks later, a music festival sale brought 80,000 buyers in ten minutes. At 12:07, app-2 froze. Health checks pulled it out in about 20 seconds, and no buyer lost a seat hold, because the holds lived in the shared store.
What does nginx do when a server freezes or returns errors?
In our tests on nginx 1.24.0, it retried a timed-out GET on the next server but not a POST, a request that changes data. It also kept sending users to a server that answered every request with a 500 error.
We put open-source nginx 1.24.0, the Ubuntu 24.04 package, in front of four small Python servers with default passive checks. We set proxy_read_timeout, how long nginx waits for a server's reply, to 3 seconds so a frozen server times out quickly. Then we froze app-3 with SIGSTOP, a signal that pauses a process.
A paused server still accepts connections, because the operating system does that, but it never answers. That mimics a hung server, which fails slowly instead of refusing connections at once.
Pause and predict: a buyer's POST to hold a seat lands on frozen app-3. Does nginx quietly retry it on app-4, as it does for a GET?
| Situation | What the user got | Why |
|---|---|---|
| A GET lands on frozen app-3 | 200 from app-4, after 3.0 s | A timeout counts as an error, and a GET is retried on the next server |
| A POST lands on frozen app-3 | 504 Gateway Time-out, after 3.0 s | By default, nginx does not resend POST, LOCK or PATCH once sent to a server |
| app-3 answers 500 to every request | 500 on 3 of 12 requests; app-3 never disabled | A 500 counts as a failure only if http_500 is listed |
| Same, with http_500 added to proxy_next_upstream | 200 on all 12 requests | The 500 is retried elsewhere and app-3 is disabled for 10 s |
| All four servers down, then restarted | 502 Bad Gateway for up to 10 more seconds | Every server stayed marked unavailable until fail_timeout ran out |
What just happened? A GET is safe to send twice, so nginx retried it and the buyer only waited. A POST may already have changed something, like holding a seat, so nginx returned the timeout instead of risking a double hold.
That rule is documented: since version 1.9.13, non-idempotent requests are not passed to the next server once sent. Idempotent means repeating it has the same effect as doing it once. If you want POST retries, make the endpoint idempotent first, for example with an idempotency key, a unique ID the client sends so the server can spot a repeat.
The 500 row surprises people. nginx's passive check counts connection errors and timeouts as failures, but not a bad answer, unless you list codes like http_500. Here is the setting we tested. In nginx, an upstream is a named group of servers.
upstream app_next500 {
server 127.0.0.1:9011;
server 127.0.0.1:9012;
server 127.0.0.1:9013;
server 127.0.0.1:9014;
}
server {
listen 127.0.0.1:8091;
location / {
proxy_pass http://app_next500;
proxy_next_upstream error timeout http_500;
}
}To check it on your own setup, send requests while one server returns 500 and search the error log for upstream server temporarily disabled. With the default setting we found no such line; with http_500 listed we found one.
The last row is the opposite of AWS's fail-open. Once every server was marked failed, nginx answered 502 and logged no live upstreams until fail_timeout ran out. The docs say only that $upstream_status records 502 when no server can be selected. They do not say how long that lasts, as of 19 October 2026, so pin your version and test it.
One documented exception: if a group has only one server, nginx never marks it unavailable. So a single-server upstream hides nothing, and its failures go straight to users.
All of the above used one nginx worker process. Workers are the separate nginx processes that handle requests. With four workers and app-3 stopped, each worker logged its own temporarily disabled line, so each one paid separately to discover the failure.
Adding a zone directive, which keeps the group's run-time state in memory shared by all workers, cut that to a single line in both of our runs. If you run several workers, add a zone so they learn about a failure together.
How fast does an active health check remove a frozen server?
We set HAProxy 2.8.16 to probe every 10 seconds with a 2-second timeout, 2 failures to leave and 3 passes to return. Over 12 runs it removed a frozen server after 14 to 24 seconds. It took the server back 20 to 30 seconds after it recovered.
That is a little slower than the team's model. The timings fit one rule: HAProxy waits 10 seconds after each probe finishes, not after it starts. We inferred that rule from the timings, not from HAProxy's code. So a timed-out probe adds its 2 seconds to the gap. The first failed probe starts 0 to 10 seconds after the freeze. The second gives up 14 seconds after the first one started, so removal lands between 14 and 24 seconds.
Coming back needs three passes in a row, 10 seconds apart, so it takes 20 to 30 seconds. Our first four runs all took 30 seconds only because the test resumed the server right after a failed probe. When we varied that moment, recovery took 20 to 30 seconds, as the rule predicts.
Now apply the same arithmetic to AWS's defaults: a 30-second interval, a 5-second timeout, 2 failures out and 5 passes in. Per load balancer node, a frozen target keeps getting some traffic for about 35 to 65 seconds. After it recovers, it waits about 2 to 2.5 minutes to return.
Those AWS numbers are our arithmetic from the documented defaults, not a measurement. One more AWS detail changes the load on your health endpoint. Each load balancer node checks every target itself, so more enabled zones mean more probes.
| Setup | Out of the pool | Back in the pool | How we know |
|---|---|---|---|
| The team's checker: a probe starts every 10 s, 2 s timeout, fall 2, rise 3 | 12 to 22 s | 20 to 30 s | Timing model run in Python 3.13 |
| HAProxy 2.8.16 with the same settings | 14.1 to 23.7 s in 12 runs | 20.1 to 30.1 s in 12 runs | Measured; the return time depends on when the server recovers |
| AWS Application Load Balancer defaults: every 30 s, 5 s timeout, 2 out, 5 in | About 35 to 65 s | About 120 to 150 s | Arithmetic from the documented defaults, not measured |
| nginx passive checks, defaults | After its first failed request | Tried again after 10 s | nginx docs and our test |
Here is the HAProxy backend we tested. The check keyword turns on active checks, and default-server sets the timer and both thresholds for every server.
backend app
balance roundrobin
option httpchk GET /healthz
timeout check 2s
default-server inter 10s fall 2 rise 3
server app-1 127.0.0.1:9021 check
server app-2 127.0.0.1:9022 check
server app-3 127.0.0.1:9023 check
server app-4 127.0.0.1:9024 checkTo check it yourself, pause one server with kill -STOP and its process ID, then watch HAProxy's log. Expect a line like Server app/app-3 is DOWN, reason: Layer7 timeout, check duration: 2001ms. After kill -CONT, an UP line follows.
How much does the algorithm matter when request times vary?
A lot, once servers get busy. In our simulation at 85 percent load, round robin's median request took 49 ms against 20 ms for least connections. Its p99, the time that 99 percent of requests beat, was 651 ms against 464.
The simulation is a made-up workload, not a benchmark. Four equal servers each run 8 workers. 80 percent of requests take 20 ms and 20 percent take 400 ms, arriving at random. We ran 200,000 requests five times per algorithm on Python 3.13.
Pause and predict: random choice and round robin both ignore how busy each server is. Which one does worse when the servers are nearly full?
| Algorithm | 60% load, p50 (median) | 60% load, p99 | 85% load, p50 (median) | 85% load, p99 |
|---|---|---|---|---|
| Round robin | 20 | 415 | 49 | 651 |
| Random | 20 | 454 | 74 | 749 |
| Least connections | 20 | 400 | 20 | 464 |
| Power of two choices | 20 | 400 | 20 | 479 |
What just happened? At 60 percent load, the median request never waited: 20 ms is just its own work, so the choice barely matters. At 85 percent, round robin keeps feeding a server stuck on seat holds, so quick page loads queue behind them. Random did worse still.
Power of two choices came within 15 ms of a full scan at p99, while looking at only two servers. It checks two servers however many you have, while a full scan checks every one.
How do you explain a load balancer in a system design interview?
Place it in your diagram early, then explain your choices in order: layer, algorithm, health checks, state and redundancy. Tie each choice to a failure it prevents.
Draw it in front of a pool
Show clients reaching one address, with a pool of identical servers behind it. Say why: spread load, hide failures and add servers easily.
Pick the layer
Choose L4 for raw speed or non-HTTP traffic, and L7 when you need path or cookie routing. Mention that many designs put L4 in front of L7.
Pick the algorithm
Use round robin for similar requests, least connections or power of two choices when request times vary, and hashing only when a key must stick to one server.
Describe health checks
Give an interval, a timeout and both thresholds, then work out how long a dead server keeps getting traffic. Mention draining for deploys and what happens when every server fails.
Handle state and retries
Keep sessions in a shared store so servers are stateless. Say which requests are safe to retry: reads yes, seat holds only with an idempotency key.
Remove the single point of failure
Run balancers in a pair or as a managed service. Add global routing across regions if the requirements ask for it.
Questions people ask
- Is a load balancer the same as a reverse proxy?
- Not exactly, but they overlap a lot. A reverse proxy receives requests on behalf of servers, and an L7 load balancer is a reverse proxy that spreads requests across several servers. An L4 load balancer never reads the HTTP request. Some L4 balancers forward packets as they are, while others, like HAProxy in TCP mode, end the connection and open their own.
- Does a load balancer make a website faster?
- It mostly makes a site more available and easier to scale, rather than making one request faster. It even adds a small extra hop. Under heavy load, though, a good algorithm keeps pages fast. In our simulation at 85 percent load, least connections cut the median request from 49 ms to 20 ms compared with round robin.
- What is the difference between a load balancer and an API gateway?
- A load balancer spreads traffic across servers and skips unhealthy ones. An API gateway sits in front of your APIs and adds features like login checks, rate limits and request changes. Many gateways also balance load, and many setups put a gateway behind a load balancer.
- How often should load balancer health checks run?
- There is no single right number, because it trades detection speed against probe traffic. AWS's Application Load Balancer defaults to every 30 seconds and allows 5 to 300. With 10-second probes and 2 failures to remove, a frozen server left our HAProxy pool in 14 to 24 seconds over 12 runs. Measure how fast your servers really fail, then tune.
- Does a load balancer retry failed requests?
- Some do, within limits. By default nginx retries on the next server after a connection error or timeout, but not a POST, LOCK or PATCH once it was sent. In our nginx 1.24.0 test, a GET to a frozen server came back from another server after 3 seconds, while a POST got a 504. Retry POSTs only if the endpoint is idempotent.
- What is the difference between DNS load balancing and a load balancer?
- DNS load balancing hands different users different addresses, while a load balancer receives every request itself. DNS is cheap and works across regions, but cached answers linger after a change. A load balancer reacts as soon as its health checks catch a failure, so large sites use DNS to pick a region and a load balancer inside it.
- Can a load balancer handle WebSockets?
- Yes. An L4 balancer carries them as ordinary TCP connections, and most L7 balancers support them too. A WebSocket is one long-lived connection, so the balancer picks a server once, when it opens. On AWS's Application Load Balancer, listener routing rules no longer apply after the upgrade, so spreading new connections evenly matters most.
Sources and further reading
- nginx documentation: Using nginx as HTTP load balancer (methods, weights, ip_hash, passive checks)
- nginx documentation: ngx_http_upstream_module (max_fails, fail_timeout, single-server rule, random two, ip_hash octets, slow_start)
- nginx documentation: ngx_http_proxy_module (proxy_next_upstream defaults and non-idempotent requests)
- Elastic Load Balancing User Guide: How Elastic Load Balancing works (NLB flow hash, ALB round robin, zone nodes)
- Application Load Balancers: Health checks for target groups (defaults, ranges, fail-open)
- Application Load Balancers: Edit target group attributes (routing algorithms, deregistration delay, slow start, AWSALB stickiness)
- Application Load Balancers: Listener rules (priority order)
- Application Load Balancers: X-Forwarded headers
- Amazon Route 53 Developer Guide: Choosing a routing policy
- Envoy documentation: Supported load balancers (weighted least request, power of two choices)
- Maglev: A Fast and Reliable Software Network Load Balancer (Eisenbud et al., NSDI 2016)
- RFC 1035: Domain names, implementation and specification (TTL, section 3.2.1)
- RFC 5798: Virtual Router Redundancy Protocol (VRRP) Version 3 (advertisement interval, Master_Down_Interval)