System Design Adventure - 7

Load Balancing: The Traffic Cop of Your System

Posted by Afsal on 07-Aug-2026

Hi Pythonistas!

Last post you added more servers.

Server1

Server2

Server3

Now you have a new problem.

A request comes in.

Which server handles it?

How does the request even know Server2 exists?

Left to themselves

all traffic goes to Server1.

Server2 and Server3 sit idle.

You need something in the middle.

That something is a Load Balancer.

What Is a Load Balancer?

A load balancer sits between the client and your servers.

                       Client

                          ↓

             Load Balancer

  ↓                     ↓                  ↓

Server1    Server2    Server3

Client never talks to your servers directly.

Client always talks to the load balancer.

Load balancer decides who handles each request.

From the client's perspective:

there is only one server.

The load balancer's IP.

What happens behind it invisible.

Does It Affect Your Application Code?

No.

Your application doesn't know a load balancer exists.

It just receives HTTP requests.

POST /users

Authorization: Bearer xyz

Content-Type: application/json

Doesn't matter if that request came through:

Nginx (software LB).
F5 BIG-IP (hardware LB).
AWS ALB (cloud LB).
No load balancer at all.

Same request. Same code handles it.

Think of it like a postal sorting office.

Letters arrive.

Sorted. Distributed to postmen.

Each postman delivers to houses.

The house (your app) just receives a letter.

Doesn't know or care about the sorting office.

One Thing That Can Affect Your Code

There is one indirect impact.

The X-Forwarded-For header.

When load balancer forwards a request:

it appears to come from the load balancer's IP.

Not the real client IP.

Without LB:

request.remote_addr = 192.168.1.5  ← real client IP

With LB:

request.remote_addr = 10.0.0.1     ← load balancer IP

If your app needs the real client IP:

client_ip = request.remote_addr

# correct  - gives real client IP

client_ip = request.headers.get('X-Forwarded-For')

Load balancer adds this header automatically:

X-Forwarded-For: 192.168.1.5

Your code reads it from headers instead.

One line change.

That's it.

Load Balancer Algorithms

How does the load balancer decide which server gets the request?

Several algorithms. Each with different tradeoffs.

Round Robin

Rotate through servers in order.

Request 1 → Server1

Request 2 → Server2

Request 3 → Server3

Request 4 → Server1

Like dealing cards around a table.

Simple. Equal distribution.

But:

doesn't consider server load.

Server1 might be handling a heavy request.

Server2 might be idle.

Round robin sends next request to Server2 regardless.

Weighted Round Robin:

Some servers are more powerful.

Give them more requests.

Server1 (8 CPU) → weight 4 → 4 out of every 6 requests

Server2 (2 CPU) → weight 1 → 1 out of every 6 requests

Server3 (2 CPU) → weight 1 → 1 out of every 6 requests

Least Connections

Send request to the server with fewest active connections.

Server1: 100 active connections

Server2: 45 active connections  ← next request goes here

Server3: 67 active connections

More intelligent than round robin.

Considers actual server load.

But:

doesn't consider how heavy each connection is.

100 light connections might be easier than 10 heavy ones.

IP Hash

Hash the client's IP address.

Result determines which server they go to.

Hash(192.168.1.5) % 3 = 1

→ always goes to Server2

Same client always hits same server.

This is called sticky sessions.

Useful if server stores session data locally.

But:

uneven distribution if some IPs generate more traffic.

One server can get overloaded.

Least Response Time

Send to the server with:

fewest active connections AND lowest average response time.

Server1: 50 connections, 200ms avg

Server2: 60 connections, 50ms avg  ← next request here

Server3: 40 connections, 300ms avg

Most intelligent basic algorithm.

Considers both load AND speed.

Consistent Hashing

More advanced.

Used heavily in distributed systems.

Hash the request (by URL, user ID, etc.)

Map result to a server on a virtual ring.

Ring:

0 ── Server1 ── Server2 ── Server3 ── 360°

Request hash = 145° → Server2

Request hash = 280° → Server3

Request hash = 40°  → Server1

Why it's special:

Add a new server?

Only a fraction of requests rerouted.

With round robin

add/remove a server → everything reshuffles.

With consistent hashing 

add/remove a server → minimal disruption.

Critical for caching systems.

We'll see it again in upcoming posts.

Layer 4 vs Layer 7

Load balancers operate at different network layers.

Layer 4 - Transport Layer

Works at TCP/UDP level.

Sees:

Source IP

Destination IP

Port

Doesn't read HTTP headers.

Doesn't know what URL is being requested.

Just forwards TCP connections.

Fast. Low overhead. Dumb.

Can't route /api to one server and /static to another.

Layer 7 - Application Layer

Works at HTTP level.

Can read:

URL path

HTTP headers

Cookies

Request body

Can make intelligent decisions:

/api/*     → route to API servers

/static/*  → route to CDN

/admin/*   → route to admin servers

Slower (relatively — still milliseconds).

Smart.

Most modern load balancers are Layer 7.

Health Checks

Load balancer constantly monitors servers.

Every few seconds:

Load Balancer → Server1: "are you alive?"

Server1: "yes, healthy" ✅

Load Balancer → Server2: "are you alive?"

Server2: "yes, healthy" ✅

Load Balancer → Server3: "are you alive?"

No response... ❌

Load Balancer: "Server3 is down. Stop sending traffic."

Active health checks:

Load balancer proactively pings:

GET /health HTTP/1.1

Response:

200 OK

{"status": "healthy"}

No response or error → server marked unhealthy.

Passive health checks:

Load balancer watches real traffic.

Server returns 500 errors → mark unhealthy.

Server too slow → mark unhealthy.

Session Persistence (Sticky Sessions)

Ideally any server handles any request.

But sometimes you need the same user on the same server.

Example:

Shopping cart stored in server's local memory.

User adds item → goes to Server1.

Next request → load balancer sends to Server2.

Server2 has no cart data.

Cart is empty.

User is angry.

Sticky sessions solve this.

Load balancer tracks which server each user is connected to.

Using a cookie:

Cookie: LB_SESSION=server1

Same user → always Server1.

But:

Server1 goes down → session lost.

This is why stateless design is better.

Store session in Redis.

Any server reads it.

No sticky sessions needed.

Software vs Hardware Load Balancers

  Software LB

Hardware LB

Cost Cheap/Free $10,000-$100,000+
Performance Good Extremely fast
Management           Your team      Vendor
Setup                       Minutes Days/weeks
Who uses it                Startups Banks, telcos

Hardware LBs are specialized machines.

Built for one job: route packets extremely fast.

Software LB (Nginx):   ~100,000 requests/second

Hardware LB (F5):      ~1,000,000+ requests/second

Processes packets at hardware level.

Before OS even sees them.

A bank processing millions of transactions per second

cannot afford 1 second of downtime.

Hardware LB worth $100,000 to them.

For a startup Nginx is perfectly fine.

Cloud Load Balancers  Best of Both:

Performance  → near hardware level

Cost         → pay per use ($0.008/hour)

Management   → zero, cloud handles it

Scaling      → automatic

This is why most modern companies use cloud LBs.

No hardware to buy.

No software to maintain.

Just configure and use.

Nginx as Load Balancer

Most developers encounter Nginx first.

Simple config:

upstream backend {

    server server1.internal:8080;

    server server2.internal:8080;

    server server3.internal:8080;

}


server {

    listen 80;

    location / {

        proxy_pass http://backend;

    }

}

Round robin by default.

Least connections:

upstream backend {

    least_conn;

    server server1.internal:8080;

    server server2.internal:8080;

    server server3.internal:8080;

}

Weighted:

upstream backend {

    server server1.internal:8080 weight=4;

    server server2.internal:8080 weight=1;

    server server3.internal:8080 weight=1;

}

Health check:

upstream backend {

    server server1.internal:8080 max_fails=3 fail_timeout=30s;

    server server2.internal:8080 max_fails=3 fail_timeout=30s;

    server server3.internal:8080 max_fails=3 fail_timeout=30s;

}

3 failures in 30 seconds → server removed temporarily.

The Load Balancer Problem

Wait.

Load balancer is now a single point of failure.

Load Balancer goes down

→ ALL servers unreachable

→ total outage

Didn't we just try to avoid this?

Solution: Multiple Load Balancers

Active-Passive:

Load Balancer 1 (active)   ← handles all traffic

Load Balancer 2 (standby)  ← takes over if LB1 fails

Active-Active:

DNS routes between:

Load Balancer 1  ← both handling traffic

Load Balancer 2

One goes down → DNS stops routing to it.

Load Balancers in a Real System

Load balancers appear everywhere.

Not just at the front.

Internet

    ↓

External Load Balancer   ← faces internet

    ↓

Web / API servers

    ↓

Internal Load Balancer   ← between services

    ↓

Microservices

    ↓

Database Load Balancer   ← between DB replicas

    ↓

Database replicas

Multiple layers.

Each independently scalable.

Mental Model

Load balancer       → distributes traffic across servers

Round robin         → rotate in order, simple

Weighted            → powerful servers get more traffic

Least connections   → server with fewest connections

IP hash             → same client always same server

Consistent hashing  → minimal reshuffling on server changes

Layer 4 LB          → TCP level, fast, dumb

Layer 7 LB          → HTTP level, smart, content-aware

Health check        → LB monitors server health

Sticky sessions     → same user always same server

Active-Passive      → one standby LB

Active-Active       → both LBs handling traffic

Software LB         → Nginx, cheap, flexible

Hardware LB         → F5, fast, expensive

Cloud LB            → managed, cheap, scales automatically

X-Forwarded-For     → get real client IP behind LB

What Changed for Me

Before this:

I thought load balancing was just "split traffic evenly."

After this:

I realized load balancing is a system in itself.

Algorithms. Health checks. Layers. Redundancy.

And the best part:

your application code doesn't change at all.

The load balancer is invisible to your app.

It just receives requests.

Does its job.

And moves on.

What's Coming Next

Now your system handles more traffic.

Multiple servers. Load balancer distributing requests.

But every request still hits the database.

Database is now the bottleneck.

How do you stop hitting the database for every single request?

Caching.

The single biggest performance improvement

you can make to any system.