1. Courses
  2. System Design
  3. Scaling Building Blocks
  4. Stop 2

35 minBeginner

Load balancers

A load balancer is the front door that spreads requests across your server copies and stops sending traffic to a copy that is sick. This lesson shows how it picks a server, how it notices failure, and the settings that cause most real outages.

By the end you can

  • Explain the difference between an L4 and an L7 load balancer
  • Choose between round robin, least connections, weighted and hash balancing
  • Set up active and passive health checks, and a real NestJS health endpoint
  • Write an Nginx upstream with failover, timeouts and the right headers
  • Spot the load balancer as a single point of failure in a diagram

The everyday picture

On a busy evening, a good floor manager stands where the checkout queues begin. She looks at the counters and sends each customer to the one that will free up first. Counter 4 has a customer with a full trolley, so she sends the next three people elsewhere. The cashier at counter 2 has gone for tea, so she sends nobody there until he is back.

She is doing three jobs: picking a counter for each customer (the balancing algorithm), checking which counters are open (health checks), and being the one place where everyone enters (the front door). If she faints, nobody knows where to go. A load balancer does the same three jobs for your servers, and has the same weakness.

What a load balancer does

A (LB) sits between clients and your server copies. Clients only know the LB's address. The LB keeps a list of backend servers, called a , and forwards each request or connection to one of them.

Because every request passes through it, the LB is also the natural place for other front-door work: holding the TLS certificate, adding a request id, limiting abusive clients (Rate limiting) and writing access logs. Nginx and HAProxy are the common self-hosted choices. AWS Application Load Balancer, Google Cloud Load Balancing and Cloudflare are common managed ones.

L4 or L7: how much of the request it reads

The names come from the layers of the network model (Networking you actually need). An works at the transport layer. It sees the client's IP address and the port, picks a server, and pipes the whole TCP connection through. It never reads the HTTP inside, which is usually encrypted anyway. That makes it fast and protocol-neutral: it can balance Postgres, MQTT or any other TCP traffic. AWS Network Load Balancer is an L4 balancer.

An works at the application layer. It decrypts HTTPS, reads the method, path, headers and cookies, and then decides. So it can send /reports/* to one pool and everything else to another, add headers, retry a failed request on another server, and see status codes for its health logic. Nginx as a reverse proxy, HAProxy in mode http and AWS ALB are L7.

One difference matters a lot in practice. An L4 balancer picks a server per connection. A mobile app with HTTP keep-alive or HTTP/2 can send hundreds of requests over one connection, and all of them land on the same server. An L7 balancer picks a server per request, so load spreads evenly even when clients reuse connections.

Quick check

You want every request for /reports/* to go to a separate pool of servers. Which balancer can do that?

L4 sees L7 sees IP: client, LB TCP port 443 TLS: encrypted GET /reports/daily headers, cookies IP: client, LB TCP port 443 TLS: decrypted here GET /reports/daily headers, cookies picks a server per connection routes each request by path or header all requests Server 1 /api /reports API pool Report pool
Both balancers see addresses and ports. Only the L7 balancer decrypts TLS and reads the path and headers, so only it can send /reports to a separate pool and spread requests from one connection across servers.

How it picks a server

The is the floor manager's rule. Four cover almost every case:

  • Round robin. Server 1, 2, 3, 1, 2, 3, in turn. It is the default in Nginx and works well when servers are the same size and requests take similar time.
  • Weighted round robin. Same idea, but a server with weight=2 gets twice as many requests. Use it while you run mixed machine sizes, for example during a migration.
  • Least connections. Send the request to the server with the fewest requests in progress right now. This adapts on its own: a slow server finishes requests slowly, keeps more in progress, and so receives fewer new ones. It is the best default when request times vary, such as a mix of quick stock lookups and slow report exports.
  • Hash (IP hash or a key hash). Hash something about the request, such as the client IP or a URL, and always map the same value to the same server. Use it when a server keeps something useful per key, such as a local cache. With the consistent option, adding a server moves only a small share of keys (Sharding and consistent hashing).

IP hash has a trap in many countries, especially where most people go online through mobile data. Mobile operators put many phones behind a small number of public IP addresses, a setup called , and all devices in one store usually share the store router's IP. Thousands of users can hash to the same server while others sit idle.

Quick check

200 store devices reach your API from behind 4 public IP addresses, and Nginx uses ip_hash across 6 servers. What happens?

AlgorithmNginx directiveGood forWatch out
Round robin(default)Equal servers, similar requestsKeeps feeding a slow server its full share
Weightedserver … weight=2Mixed machine sizesWeights go stale when machines change
Least connectionsleast_conn;Mixed fast and slow requestsA server that fails fast looks "free" and attracts traffic, so pair it with health checks
IP haship_hash;Simple stickinessUneven behind NAT; users move when servers change
Key hashhash $request_uri consistent;Per-key cache localityHot keys overload one server

Health checks: skipping the counter on a break

A decides who is in the pool. There are two kinds, and good setups use both.

Active checks. The LB calls each server on its own schedule, for example GET /health/ready every 2 seconds. After a set number of failures in a row (often 2 or 3) it removes the server. After a set number of passes it adds it back. HAProxy and managed balancers do this. In Nginx, active checks are part of the paid NGINX Plus; open-source Nginx only has passive checks.

Passive checks. The LB watches real traffic. If requests to a server fail to connect or time out a few times within a window, it stops using that server for a while. Nginx's max_fails and fail_timeout work this way. The cost is that a few real user requests must fail before the LB reacts.

What the health endpoint tests matters. A check asks "is the process running?" and is used to restart a stuck container. A check asks "can this copy serve requests right now?" and is what the LB should use. A copy that has started but has not connected to the database yet is alive and not ready.

Quick check

A new copy answers HTTP, but its database connection is still being set up. What should its checks say?

Clients HTTPS Load balancer traffic GET /health Server 1 healthy Server 2 healthy Server 3 503: out Server 3 checks 1/2 2/2 0 s 2 s 4 s 6 s removed
The LB checks every server every 2 seconds. Server 3 fails at 4 s and again at 6 s, so after 2 failures in a row it leaves the pool. Servers 1 and 2 take all traffic until server 3 passes a check again.
Think first: your readiness check pings the database. The database restarts and is gone for 10 seconds. What does the load balancer do?

Every copy fails its check at the same moment, so the LB removes all of them. Users get the LB's own 502 or 503 page instead of your app's error message, and when the database returns, all copies rejoin together. That can be acceptable, because the app could not serve anyway. Many teams keep the database ping but give it a short timeout (about 1 second) and require 2 or 3 failures, so a short blip does not empty the pool. What you must never do is put a slow or optional dependency, such as an SMS gateway, in the readiness check. One slow partner would take your whole API out.

Try it yourself. Start with round robin and press "Send 20 at once". Then tick "Server 2 is slow", turn on "Steady traffic" and switch between round robin and least connections. Finally tick "Server 3 crashes" and watch the health checks remove it.

Simulator

Loading the simulator…

  • With a slow server, round robin still gives it a third of the requests, so its queue grows. Least connections sends it fewer because it is always "busy".
  • A crashed server causes failed requests until the second failed check. That gap is why the check interval and failure count matter.

Sticky sessions, TLS and timeouts

Sticky sessions. A setting pins a user to one server, usually with a cookie the LB sets. It hides in-memory state instead of removing it, makes load uneven, and logs users out when their server is redeployed. Make the app stateless instead (Scaling up vs scaling out). One real exception: Socket.IO's HTTP long-polling fallback needs all requests of one client to reach the same server (Real-time updates).

TLS termination. The LB holds the certificate, decrypts HTTPS, and talks plain HTTP to the servers inside the private network. You renew certificates in one place, and the LB can read the request for L7 routing. Because the app now sees the LB as the client, the LB must pass the original details in headers: X-Forwarded-For (client IP) and X-Forwarded-Proto (was it https). The app must trust those headers only from the LB.

Timeouts. Every LB has a connect timeout and a read timeout. If the LB's read timeout is 60 seconds and a monthly report takes 90, the user gets a 504 Gateway Timeout while the server keeps working and may finish a report nobody receives. Long work belongs in a background job (Queues and background jobs). The other classic bug is the idle timeout, below.

The load balancer is a single point of failure

You added three servers so that one can die. Now the LB is the one box that cannot. There are three common answers:

  • A managed load balancer. AWS ALB, Google Cloud and Cloudflare run their balancers across several machines and zones for you. For most teams this is the right answer.
  • Two self-hosted LBs with a floating IP. Two Nginx or HAProxy machines share one , managed by keepalived. If the active one dies, the standby takes the address within a few seconds.
  • Several DNS records. Publish two LB addresses in DNS. It is cheap but slow to react, because clients and resolvers cache DNS answers for the TTL (What happens when you open an app).

applies the same idea one level up. A DNS service or an anycast network sends each user to the nearest healthy region and moves traffic to another region if one fails. Most apps serving one country run in one region and do not need it yet. They do need a plan for what happens if that region or data centre has a bad day (Backups and disaster recovery).

Build it

Put Nginx in front of three API copies with least connections, passive health checks, sane timeouts and correct headers, add a real readiness endpoint, then see what active checks look like in HAProxy. To practise the two most common picking rules in code, try the JavaScript exercise in the Practice section; it runs in your browser.

  1. Define the pool

    max_fails=3 fail_timeout=10s means: if 3 attempts fail within 10 seconds, skip this server for the next 10 seconds, then try it again.

    nginx
    # /etc/nginx/conf.d/api.conf
    upstream api_pool {
        least_conn;                     # fewest in-progress requests wins
    
        server 10.0.1.11:3000 max_fails=3 fail_timeout=10s;
        server 10.0.1.12:3000 max_fails=3 fail_timeout=10s;
        server 10.0.1.13:3000 max_fails=3 fail_timeout=10s weight=2;  # bigger machine
    
        keepalive 32;                   # idle connections kept open to the pool
    }
    
    upstream report_pool {
        least_conn;
        server 10.0.1.21:3000 max_fails=3 fail_timeout=10s;
        server 10.0.1.22:3000 max_fails=3 fail_timeout=10s;
    }
  2. Terminate TLS, route by path, set headers and timeouts

    proxy_next_upstream lists which failures make Nginx try the next server. Connection errors and timeouts always count as failures for max_fails; 502 and 503 count only if you list them. Since Nginx 1.9.13, a POST that already reached a server is not retried elsewhere unless you add non_idempotent, which protects you from recording the same delivery twice (APIs done right).

    nginx
    server {
        listen 443 ssl;
        server_name api.example.com;
        ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    
        proxy_http_version 1.1;
        proxy_set_header Connection "";                    # allow upstream keepalive
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Request-Id $request_id;         # trace one request in logs
    
        proxy_connect_timeout 2s;      # a healthy server on a LAN accepts in ms
        proxy_send_timeout    30s;
        proxy_read_timeout    30s;     # max wait between two reads, not total time
    
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;   # at most one retry on another server
        proxy_next_upstream_timeout 5s;
    
        location /reports/ {
            proxy_pass http://report_pool;
            proxy_read_timeout 120s;   # exports are slow; better: make them jobs
        }
    
        location / {
            proxy_pass http://api_pool;
        }
    }
    Terminal
    sudo nginx -t && sudo systemctl reload nginx   # test config, then reload with no downtime
  3. Tell NestJS it is behind a proxy

    Without trust proxy, req.ip is the LB's address for every user, which breaks logging and per-IP rate limits. Raising keepAliveTimeout fixes the random 502s from the callout above.

    TypeScript
    // main.ts
    import { NestFactory } from '@nestjs/core';
    import { NestExpressApplication } from '@nestjs/platform-express';
    import { AppModule } from './app.module';
    
    async function bootstrap() {
      const app = await NestFactory.create<NestExpressApplication>(AppModule);
      app.set('trust proxy', 1);   // trust ONE hop: our LB sets X-Forwarded-For
      app.enableShutdownHooks();
    
      const server = app.getHttpServer();
      server.keepAliveTimeout = 65_000;  // longer than the LB idle timeout (60 s)
      server.headersTimeout = 66_000;    // must be larger than keepAliveTimeout
    
      await app.listen(3000);
    }
    bootstrap();
  4. Add liveness and readiness with @nestjs/terminus

    Install with npm i @nestjs/terminus and add TerminusModule to the module's imports. PrismaHealthIndicator is built into Terminus 10 and later. When a check fails, @HealthCheck() makes the endpoint return 503, which is exactly what the LB needs to see.

    TypeScript
    // health.controller.ts
    import { Controller, Get } from '@nestjs/common';
    import { HealthCheck, HealthCheckService, PrismaHealthIndicator } from '@nestjs/terminus';
    import { PrismaService } from '../prisma/prisma.service';
    
    @Controller('health')
    export class HealthController {
      constructor(
        private readonly health: HealthCheckService,
        private readonly prismaHealth: PrismaHealthIndicator,
        private readonly prisma: PrismaService,
      ) {}
    
      // Liveness: is the process responding? No dependencies on purpose.
      @Get('live')
      live() {
        return { status: 'ok' };
      }
    
      // Readiness: can this copy serve traffic? The LB checks this one.
      @Get('ready')
      @HealthCheck()
      ready() {
        return this.health.check([
          () => this.prismaHealth.pingCheck('database', this.prisma, { timeout: 1000 }),
        ]);
      }
    }
    Output
    $ curl -s localhost:3000/health/ready
    {"status":"ok","info":{"database":{"status":"up"}},"error":{},"details":{"database":{"status":"up"}}}
    
    # with Postgres stopped: HTTP 503
    {"status":"error","info":{},"error":{"database":{"status":"down"}},"details":{"database":{"status":"down"}}}
  5. See active health checks in HAProxy

    If you need active checks without NGINX Plus, HAProxy has them built in. inter 2s fall 2 rise 1 means: check every 2 seconds, remove after 2 failures in a row, add back after 1 pass. That is the same rule the simulator uses. The http-check send syntax needs HAProxy 2.2 or later.

    Terminal
    sudo tee /etc/haproxy/haproxy.cfg > /dev/null <<'EOF'
    defaults
        mode http
        timeout connect 2s
        timeout client  30s
        timeout server  30s
        option forwardfor                  # adds X-Forwarded-For
    
    frontend web
        bind :80
        default_backend api
    
    backend api
        balance leastconn
        option httpchk
        http-check send meth GET uri /health/ready
        http-check expect status 200
        default-server inter 2s fall 2 rise 1
        server api1 10.0.1.11:3000 check
        server api2 10.0.1.12:3000 check
        server api3 10.0.1.13:3000 check
    EOF
    haproxy -c -f /etc/haproxy/haproxy.cfg   # prints "Configuration file is valid"
    sudo systemctl reload haproxy
  6. Break a server on purpose

    Stop the API on one machine and send traffic. Nginx logs the failed attempts, skips that server for fail_timeout, and users of the other two notice nothing.

    Terminal
    # on 10.0.1.13
    sudo systemctl stop api
    
    # from anywhere: 20 requests, print status codes
    for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code} " https://api.example.com/health/ready; done; echo
    
    # on the LB: what Nginx saw
    sudo tail -n 3 /var/log/nginx/error.log
    Output
    200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
    ... connect() failed (111: Connection refused) while connecting to upstream,
        upstream: "http://10.0.1.13:3000/health/ready" ...

    All 200s. "Connection refused" happens before any request bytes are sent, so Nginx can safely try the next server, even for a POST. A server that crashes in the middle of a request is different: that request was already sent, so Nginx does not resend a POST, and that one write fails. That is fine as long as the app retries with an idempotency key (APIs done right).

Bring it to your team

Ask these when you see a box labelled "LB" in a design.

  • Is this an L4 or an L7 balancer, and which algorithm does it use? Why that one for our mix of requests?
  • What does the health check call, how often, and after how many failures is a server removed? How many user requests fail before that?
  • Does the readiness check depend on anything slow or optional, like the SMS or payment gateway?
  • What are the LB's read and idle timeouts, and what is our slowest endpoint? Is the Node keep-alive timeout longer than the LB's idle timeout?
  • If the load balancer itself dies, what takes over, and how long does it take?
  • Do we need sticky sessions anywhere? If yes, what state is the server keeping that it should not?

Red flags

  • The health check returns 200 from a static route even when the database is unreachable.
  • One self-hosted Nginx box in front of everything, with no standby.
  • Report exports or bulk uploads that regularly hit the LB's 60-second timeout.
  • Every log line shows the LB's IP as the client IP.
  • Sticky sessions turned on because "users kept getting logged out".

Practice

Write round robin and least connections

Each server is an object such as { name: 'api1', healthy: true, active: 3 }, where active is the number of requests in progress. Write createRoundRobin(servers), which returns a pick() function that hands out healthy servers in turn and skips unhealthy ones, and leastConnections(servers), which returns the healthy server with the fewest active requests (the first one in the list wins a tie). Both return the server's name, or null when no server is healthy. Read healthy on every call, because health checks change it at any time.

JavaScript exerciseRuns in your browser with tests
function createRoundRobin(servers) {
  let next = 0; // index to try first on the next pick
  return function pick() {
    // start at next, skip unhealthy servers, wrap around the end of the list
    // return the chosen server's name, or null if none is healthy
  };
}

function leastConnections(servers) {
  // the healthy server with the fewest active requests; the first one wins a tie
  // return its name, or null if none is healthy
}
Ctrl + Enter runs
Show a solution
JavaScript
function createRoundRobin(servers) {
  let next = 0;
  return function pick() {
    for (let i = 0; i < servers.length; i++) {
      const idx = (next + i) % servers.length;      // wrap around the end
      if (servers[idx].healthy) {
        next = (idx + 1) % servers.length;          // the turn after this one
        return servers[idx].name;
      }
    }
    return null;                                    // nobody healthy: the LB returns 502/503
  };
}

function leastConnections(servers) {
  let best = null;
  for (const s of servers) {
    if (!s.healthy) continue;
    if (best === null || s.active < best.active) best = s;   // strict: first one wins a tie
  }
  return best ? best.name : null;
}
Pass the exercise to finish this task.

Line up the timeouts

Mangrove's API runs behind an AWS Application Load Balancer with its default 60-second idle timeout. Node's keepAliveTimeout is still the default 5 seconds, and users report a few random 502s a day. The slowest normal endpoint takes about 8 seconds. The monthly export takes about 90 seconds and often ends in a 504. Write the settings you would change, with one sentence each on why.

  • You set Node's keepAliveTimeout above the balancer's idle timeout (for example 65 seconds), with headersTimeout a little higher
  • You explained the random 502s: the balancer reuses an idle connection at the moment Node closes it
  • You left the balancer's timeout comfortably above the slowest normal request instead of raising it for every route
  • You moved the 90-second export to a background job that returns 202 and a status link, or at least gave it its own route and pool
Show a model answer

Node: server.keepAliveTimeout = 65_000 and server.headersTimeout = 66_000. The balancer keeps idle connections for 60 seconds; Node must not close them first, or a request sent on a closing connection fails with a 502.

Balancer: keep the 60-second idle timeout. The slowest normal request (8 seconds) fits easily, and a long timeout for everything would hide slow code and hold connections open.

Export: answer 202 Accepted with a job id, build the file in a background worker, and let the app download it when it is ready. A 90-second request will keep failing at some proxy, whatever you raise.

Check yourself

One of your three servers has become slow (a noisy neighbour on its VM). Which algorithm keeps user latency lowest without any manual change?

Your mobile app keeps one HTTP/2 connection open and sends all its requests over it. The LB in front is an L4 network balancer. What happens?

Which health check is the best fit for the load balancer's readiness check?

Flashcards

Open my review

Key takeaways

  • A load balancer picks a server, removes sick ones, and is the front door. L4 balances connections; L7 reads HTTP and balances each request.
  • Least connections is a strong default when request times vary. Avoid IP hash behind NAT.
  • Use a fast readiness check (503 on failure) for the LB and a dependency-free liveness check for restarts. Active checks catch failures before users do; passive checks need some failures first.
  • Align timeouts: LB read timeout above your slowest normal request, and Node's keep-alive timeout above the LB's idle timeout.
  • The LB itself must be redundant: managed, a pair with a floating IP, or at least a plan.

Sources

Episode transcriptThe floor manager at the front door · about 14 minutes · tap any line to play from there
  1. NadiaImran, you said you had a mystery for me today.
  2. ImranI do. Our app shows a five oh two error now and then. Nobody can reproduce it. The servers look fine, the logs look fine.
  3. NadiaIs there a managed load balancer in front, like an AWS one?
  4. ImranYes. Why?
  5. NadiaBecause that exact mystery is in today's lesson, and the cause is two timeouts that disagree. We'll get there. Today is about the load balancer: the front door that spreads requests across your server copies, and stops sending traffic to a copy that's sick.
  6. NadiaLast episode we left a floor manager at the checkout. Let's watch her properly. She stands where the queues begin, looks at the counters, and sends each customer to the one that will free up first.
  7. ImranSo if counter four has a customer with a full trolley, she sends the next three people somewhere else.
  8. NadiaExactly. And if the cashier at counter two has gone for tea, she sends nobody there until he's back. So she does three jobs. Picking a counter for each customer, which is the balancing algorithm. Checking which counters are open, which is health checks. And being the one place where everyone enters, the front door.
  9. ImranAnd if she faints, nobody knows where to go.
  10. NadiaA load balancer does the same three jobs for your servers, and has the same weakness. Clients only know its address. It keeps a list of backend servers, called a pool, and forwards each request to one of them.
  11. ImranOur developers talk about L4 and L7 balancers. What's the difference?
  12. NadiaIt's about how much of the request it reads. An L4 balancer works at the transport layer. It sees IP addresses and the port, picks a server, and pipes the whole connection through. It never reads the HTTP inside, which is usually encrypted anyway. So it's fast, and it can balance anything, even Postgres traffic.
  13. NadiaAn L7 balancer works at the application layer. It decrypts HTTPS and reads the method, the path, the headers and cookies, then decides. So it can send all report requests to one pool and everything else to another, add headers, and retry a failed request on another server.
  14. NadiaThe first diagram puts them side by side. If you can't see it, picture two lists of what each one sees. Both see addresses and port four four three. Only the L7 one sees the decrypted path, like slash reports slash daily. So only L7 can route that request to a separate report pool.
  15. ImranSo L4 is the cheaper, simpler one. For an HTTP app, it shouldn't matter much.
  16. NadiaIt matters more than you'd think. An L4 balancer picks a server per connection. A mobile app with HTTP two can send hundreds of requests over one connection, and every one lands on the same server. An L7 balancer picks a server per request, so the load spreads evenly even when apps reuse connections.
  17. ImranOkay. Now the floor manager's rule. How does it pick?
  18. NadiaFour rules cover almost everything. Round robin: server one, two, three, one, two, three. It's the default in Nginx, and fine when servers are the same size and requests take similar time. Weighted round robin: a server with weight two gets twice as many. Useful while you run mixed machine sizes.
  19. NadiaLeast connections: send the request to the server with the fewest requests in progress right now. It adapts on its own. A slow server finishes slowly, so it has more in progress, so it gets fewer new ones. It's the best default when request times vary, like quick stock lookups mixed with slow exports.
  20. ImranAnd the fourth?
  21. NadiaHash. You hash something about the request, like the client IP or the link, and always send the same value to the same server. Useful when a server keeps a local cache per key.
  22. ImranIP hash sounds nice for us. Each store always goes to the same server.
  23. NadiaThat's the trap in many countries, especially where most people go online by mobile data. Mobile operators put many phones behind a few public IP addresses. That's called CGNAT, carrier-grade NAT.
  24. NadiaAnd every device in one store usually shares the store router's IP. So thousands of users can hash to one server while others sit idle.
  25. ImranSo IP hash gives us uneven load exactly when we're busy. Noted.
  26. NadiaNow the second job: checking who's open. Health checks come in two kinds. Active checks: the balancer calls each server on its own schedule, say every two seconds. After two or three failures in a row, it removes the server. After a pass, it adds it back.
  27. NadiaPassive checks: the balancer watches real traffic. If requests to a server fail or time out a few times within a window, it stops using that server for a while. That's what open-source Nginx has. The cost is that a few real user requests must fail first.
  28. NadiaThe second diagram shows active checks over time. Picture three servers, checked every two seconds. Server three fails at four seconds, and again at six. After two failures in a row, it's out of the pool. Servers one and two carry all the traffic until server three passes a check again.
  29. ImranWhat should the health check actually test?
  30. NadiaThere are two kinds of question. A liveness check asks, is the process running? That's used to restart a stuck container. A readiness check asks, can this copy serve requests right now? That's the one the balancer should use. A copy that has started but hasn't connected to the database yet is alive, but not ready.
  31. ImranThen the readiness check should test everything. Database, Redis, the SMS gateway, the payment API. To be safe.
  32. NadiaPlease don't, Imran. If the SMS gateway gets slow, every copy fails its check at once, and the balancer removes your whole API. One slow partner turns a small problem into a full outage. Readiness should test what this copy needs, quickly. Usually a database ping with about a one second timeout.
  33. ImranAnd the opposite mistake?
  34. NadiaA route that always says okay, even when the database is unreachable. Then a broken copy stays in the pool forever.
  35. NadiaThis is a good one to play with later, in the simulator. Start with round robin and send twenty requests at once. Then mark server two as slow, turn on steady traffic, and switch between round robin and least connections. Finally, make server three crash, and watch the health checks remove it.
  36. ImranAnd I'll see round robin keep feeding the slow server its full third.
  37. NadiaYes, while least connections sends it fewer. You'll also see failed requests after the crash, until the second failed check. That gap is why the check interval and the failure count matter.
  38. ImranNow, Nadia, my five oh two mystery.
  39. NadiaThe balancer keeps idle connections open to your servers and reuses them. An AWS application load balancer keeps them for sixty seconds by default. But Node's HTTP server closes idle connections after only five seconds.
  40. ImranSo now and then, the balancer sends a request on a connection at the exact moment Node closes it.
  41. NadiaAnd the user sees a five oh two nobody can reproduce. The fix is one setting: make Node's keep-alive timeout longer than the balancer's idle timeout.
  42. ImranAny other timeout trap?
  43. NadiaThe read timeout. If it's sixty seconds and a monthly report takes ninety, the user gets a five oh four, gateway timeout. Meanwhile the server keeps working and may finish a report nobody receives. Long work belongs in a background job.
  44. ImranSpeaking of reports, this is our real problem. The same API pool serves the Store app and the back office dashboards.
  45. NadiaThe lesson uses that exact setup. At month end, buyers and head office export big sales and delivery reports, each taking twenty to forty seconds. With round robin, every server gets its share of slow exports, and a store manager's quick stock lookup waits behind them.
  46. ImranSo least connections helps. And an L7 rule could send report requests to their own small pool.
  47. NadiaExactly. Then heavy exports can never slow down the stores. That's the routing in the first diagram.
  48. NadiaTwo more things the balancer does. It usually holds the TLS certificate, decrypts HTTPS, and talks plain HTTP to servers inside the private network.
  49. NadiaAnd because the app now sees the balancer as the client, the balancer passes the real client IP in a forwarded-for header. The app must trust that header only from the balancer.
  50. ImranAnd what if the floor manager faints? The balancer itself dies?
  51. NadiaYou added three servers so one can die. Now the balancer is the box that can't. For most teams, the answer is a managed balancer, which runs across several machines and zones for you. Or two self-hosted ones sharing a floating IP address. The standby takes over within seconds.
  52. ImranWhat does the Build it section set up?
  53. NadiaNginx in front of three API copies. First, a pool using least connections, with passive checks: three failures within ten seconds, and the server is skipped for ten seconds. One bigger machine gets weight two. And a separate report pool.
  54. NadiaThen the front door itself. Nginx holds the certificate, passes the client IP and a request ID in headers, and has a two-second connect timeout and a thirty-second read timeout. Report paths go to the report pool with a longer timeout. And it retries a failed request on another server at most once.
  55. ImranRetries worry me. Could that record the same delivery twice?
  56. NadiaGood instinct. Nginx won't resend a POST that already reached a server, unless you tell it to. That protects you. Then in NestJS, you tell Express it's behind one proxy, so it reads the real client IP. And you raise the keep-alive timeout to sixty-five seconds, above the balancer's sixty.
  57. NadiaNext, real liveness and readiness endpoints with the NestJS Terminus package. Readiness pings the database with a one-second timeout, and returns five oh three if it fails. Then the same setup in HAProxy with active checks, every two seconds, out after two failures. And finally you stop one server on purpose.
  58. ImranAnd the users notice?
  59. NadiaIn the lesson's test, twenty requests, all two hundreds. A refused connection happens before any request is sent, so Nginx safely tries the next server, even for a POST.
  60. ImranCan I try the picking rules myself, without a server farm?
  61. NadiaYes. In the Practice section you write round robin and least connections in a few lines of JavaScript, and the page tests them for you. A second task asks you to line up the timeouts behind those random five oh twos.
  62. ImranMy review questions, then. Is this an L4 or an L7 balancer, and which algorithm, and why for our mix? What does the health check call, how often, and how many user requests fail before a server is removed? Does readiness depend on anything slow, like the SMS gateway?
  63. NadiaAnd: if the load balancer itself dies, what takes over, and how long does it take?
  64. NadiaHere's a puzzle. Your readiness check pings the database. The database restarts, and is gone for ten seconds. What does the balancer do? Take a second.
  65. ImranEvery copy fails at the same moment. So it removes all of them.
  66. NadiaRight. Users see the balancer's own error page, and when the database returns, all copies rejoin together. That can be fine, because the app couldn't serve anyway. Many teams keep the ping, but require two or three failures, so a short blip doesn't empty the pool.
  67. ImranTakeaways. One. A load balancer picks a server, removes sick ones, and is the front door. L4 balances connections, L7 reads HTTP and balances each request. Two. Least connections is a strong default when request times vary, and IP hash is a bad idea behind shared mobile IPs.
  68. ImranThree. Readiness checks should be fast and test only what the copy needs. And Node's keep-alive timeout must be longer than the balancer's idle timeout.
  69. NadiaGood. My addition: the balancer itself must be redundant. Managed, a pair with a floating IP, or at least a written plan.
  70. ImranI'm sending that keep-alive fix to the team tonight.
  71. NadiaNext episode is about not asking the database at all: caching, and the stock count that's always slightly wrong.
Finished reading?Mark it complete to fill in its stop on your course line.

Use it at work

3-tier web app

Clients, a load balancer, app servers and a database.

Open in the System Design Studio
Next stopCaching
Was this lesson helpful?
Questions and discussion

Loading the discussion…