Skip to content

Lab: Load Balancer Algorithms

Pairs with: Load Balancing

Three real backends (two fast, one deliberately slow) behind a real nginx upstream block. Swap round robin for weighted or least-connections by editing config and reloading — the same way you would in production — kill a backend and watch nginx route around it with zero client-visible errors.

docker-compose.yml

name: labs-load-balancer

services:
  backend1:
    image: python:3.12-slim
    command: ["python", "/app/backend.py"]
    environment:
      BACKEND_ID: backend1
    volumes:
      - ./backend.py:/app/backend.py:ro
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 3s
      timeout: 3s
      retries: 20

  backend2:
    image: python:3.12-slim
    command: ["python", "/app/backend.py"]
    environment:
      BACKEND_ID: backend2
    volumes:
      - ./backend.py:/app/backend.py:ro
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 3s
      timeout: 3s
      retries: 20

  backend3:
    image: python:3.12-slim
    command: ["python", "/app/backend.py"]
    environment:
      BACKEND_ID: backend3
      # This backend is deliberately slow — pairs with the least_conn exercise.
      SLEEP_MS: "300"
    volumes:
      - ./backend.py:/app/backend.py:ro
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 3s
      timeout: 3s
      retries: 20

  lb:
    image: nginx:1.27
    depends_on:
      backend1:
        condition: service_healthy
      backend2:
        condition: service_healthy
      backend3:
        condition: service_healthy
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "8080:80"

nginx.conf

worker_processes 1;
events { worker_connections 1024; }

http {
    # Exercise 1-2: default round robin. Comment this block and uncomment
    # one of the alternatives below, then `docker exec labs-load-balancer-lb-1 nginx -s reload`.
    upstream backend_pool {
        server backend1:8000 max_fails=2 fail_timeout=5s;
        server backend2:8000 max_fails=2 fail_timeout=5s;
        server backend3:8000 max_fails=2 fail_timeout=5s;
    }

    # Exercise 4: weighted round robin — backend1 gets 5x the traffic.
    # upstream backend_pool {
    #     server backend1:8000 weight=5 max_fails=2 fail_timeout=5s;
    #     server backend2:8000 weight=1 max_fails=2 fail_timeout=5s;
    #     server backend3:8000 weight=1 max_fails=2 fail_timeout=5s;
    # }

    # Exercise 5: least connections — routes to whichever backend has the
    # fewest active connections, instead of blindly rotating.
    # upstream backend_pool {
    #     least_conn;
    #     server backend1:8000 max_fails=2 fail_timeout=5s;
    #     server backend2:8000 max_fails=2 fail_timeout=5s;
    #     server backend3:8000 max_fails=2 fail_timeout=5s;
    # }

    server {
        listen 80;
        location / {
            proxy_pass http://backend_pool;
            proxy_next_upstream error timeout http_502 http_503 http_504;
            proxy_connect_timeout 1s;
        }
    }
}

Exercises

The full walkthrough (verify round robin, kill a backend, swap to least-connections and weighted, measure the distribution difference) lives in the lab's README — including a real bind-mount caching gotcha on macOS/Colima worth knowing about:

labs/load-balancer/README.md on GitHub

git clone https://github.com/sanketn26/interview-prep
cd interview-prep/labs/load-balancer
docker compose up -d

← All Labs