One Server is a Risk, Two is a Solution: Load Balancing Hands On (practice for better understanding)
Author : Nahid Hasan (Senior Software Engineer)

One Server is a Risk, Two is a Solution: Load Balancing Hands On (practice for better understanding)

Most developers have heard the term “Load Balancer.”

But very few actually build one themselves and see how it behaves in real time.

Last week, I decided to do exactly that.

I set up a working load balancing system on my local machine using Laravel 12, Docker, Nginx, MySQL, and Redis. No cloud, no expensive infrastructure — just a laptop and curiosity.

In this post, I’ll walk you through how it works in the simplest way possible.


What is a Load Balancer?

Let’s make this very simple.

Imagine a fast-food restaurant.

If there’s only one cashier and ten customers, everything slows down.

Now add a second cashier. Customers get split between them, and things move faster.

A load balancer is like the manager who decides which cashier will serve which customer.

In web applications, when thousands of users hit your app, a load balancer distributes those requests across multiple servers so no single server gets overwhelmed.


What I Built

Here’s the architecture:

Browser → Load Balancer (Nginx)
              ↓           ↓
          Server 1     Server 2    ← Both run Laravel 12
              ↓           ↓
           MySQL       Redis       ← Shared between both        

Both Laravel servers use the same database and the same session storage.

So if a user logs in through Server 1, they will still be logged in even if the next request goes to Server 2.


The Two Types of Load Balancers

This part confused me a lot in the beginning, so let me explain it clearly.

L4 — Layer 4 (Transport Layer)

Think of this as a very fast but simple system.

It only looks at network-level information like IP and port.

It does not understand what’s inside the request.

Like a postman who only reads the address on the envelope and delivers it without opening it.

  • Works with TCP/UDP
  • Cannot read URLs, headers, or cookies
  • Very fast
  • Used by MySQL and Redis in this setup


L7 — Layer 7 (Application Layer)

This one is smarter.

It actually understands HTTP requests.

It can read URLs, headers, cookies, and make decisions based on them.

Like a postman who opens the letter, reads it, and decides where it should go.

  • Works at HTTP level
  • Can inspect requests deeply
  • Can route based on logic
  • Slightly slower than L4
  • Used by Nginx as the load balancer


The Actual Code Difference

L7 Load Balancer (Nginx — HTTP Layer)

Layer 7 Nginx reads HTTP requests, inspects headers, and forwards requests intelligently:

upstream laravel_cluster {
    server app_node_1:80;
    server app_node_2:80;
}

server {
    listen 80;

    location / {
        proxy_pass http://laravel_cluster;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}        

  • Nginx can read HTTP headers and route requests based on content.
  • This is Layer 7 behavior — “smart” routing.


L4 Load Balancer (Nginx — TCP Layer, Round-Robin)

Layer 4 Nginx works purely at the TCP layer. It forwards connections, not requests, and cannot inspect headers:

stream {
    upstream laravel_cluster_tcp {
        server app_node_1:80;
        server app_node_2:80;
        # Round-robin is the default
    }

    server {
        listen 80;

        proxy_pass laravel_cluster_tcp;
    }
}        

  • Each new TCP connection is sent to the next server in order (round-robin).
  • Existing connections stay on the same server.
  • No headers, no cookies, no HTTP logic — just raw TCP packets.
  • Extremely fast, but “dumb” compared to Layer 7.

L4 Example (MySQL and Redis)

mysql:
  image: mysql:8.4
  ports: ["3306:3306"]

redis:
  image: redis:alpine        

There are no headers here. No HTTP.

Just raw TCP connections.

That’s Layer 4.


This shows clearly:

  • L7: Smart, request-based routing, can inspect headers.
  • L4: Fast, connection-based routing, round-robin by default.


How the Laravel Apps Are Exposed

This is something many tutorials skip, but it’s very important.

The Laravel apps are not exposed to your browser.

Only the load balancer is.

Think of it like this:

You (Browser)
      ↓
Load Balancer (only entry point)
      ↓           ↓
App 1         App 2        

Now look at this:

nginx_lb:
  ports: ["8000:80"]

app_node_1:
app_node_2:        

Only the load balancer has a public port.

The app servers are completely hidden inside Docker’s internal network.

Docker automatically gives each container a name, and they communicate using those names internally.

So this works:

server app_node_1:80;
server app_node_2:80;        

But you cannot open them in your browser directly.


What Runs Inside Each Laravel Container

Each Laravel container is not just Laravel.

It actually runs two processes:

Nginx     → handles HTTP requests
PHP-FPM   → executes PHP code        

Started with:

CMD php-fpm -D && nginx -g 'daemon off;'        

And internally:

location ~ \.php$ {
    fastcgi_pass 127.0.0.1:9000;
}        

Nginx forwards PHP requests to PHP-FPM.


Full Request Flow

Here’s what happens when you open the app:

1. Browser hits http://localhost:8000

2. Request goes to Load Balancer (Nginx)
   → Decides which server to use

3. Request goes to app_node_1 or app_node_2
   → Nginx inside container receives it

4. PHP requests go to PHP-FPM
   → Laravel executes

5. Laravel talks to:
   → MySQL (database)
   → Redis (sessions)

6. Response comes back:
   → App → Load Balancer → Browser        

There are actually three Nginx instances:

  • One as Load Balancer
  • One inside app_node_1
  • One inside app_node_2

This is completely normal.


The Shared Session Problem

Here’s a real issue:

User logs in on Server 1 Next request goes to Server 2 Server 2 doesn’t know the session

User gets logged out.

To fix this, both servers must share sessions.

SESSION_DRIVER=redis
REDIS_HOST=redis        

Now both servers read and write sessions from Redis.

Problem solved.


How to Test It

Add this route to your web.php :

Route::get('/debug', fn() => "Server: " . env('NODE_NAME'));        

Each container has a different name.

Now refresh:

http://localhost:8000/debug        

You’ll see:

Server: Alpha
Server: Bravo
Server: Alpha
Server: Bravo        

That means your load balancer is working.

Github link : https://github.com/nahidprince7/load-balancer-laravel-nginx-redis

follow README.md

If you like it give a star to the repository .Thanks.

To view or add a comment, sign in

More articles by Nahid Hasan

Others also viewed

Explore content categories