A simple Layer 7 (HTTP) load balancer built in Go that forwards requests to backend servers transparently — the request reaches the backend exactly as it came from the client, and the response goes back to the client exactly as it came from the backend.
%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#2563eb', 'actorBorder': '#1d4ed8', 'actorTextColor': '#ffffff', 'actorLineColor': '#93c5fd', 'signalColor': '#ffffff', 'signalTextColor': '#ffffff', 'noteBkgColor': '#fef08a', 'noteTextColor': '#713f12', 'activationBkgColor': '#bfdbfe', 'activationBorderColor': '#2563eb'}}}%%
sequenceDiagram
participant C as Client
participant LB as Load Balancer (:8080)
participant B1 as Backend :8081
participant B2 as Backend :8082
participant B3 as Backend :8083
C->>LB: Request 1
Note over LB: Round Robin selects :8081
LB->>B1: Forward cloned request
B1-->>LB: HTTP Response
LB-->>C: Forward response
C->>LB: Request 2
Note over LB: Round Robin selects :8082
LB->>B2: Forward cloned request
B2-->>LB: HTTP Response
LB-->>C: Forward response
C->>LB: Request 3
Note over LB: Round Robin selects :8083
LB->>B3: Forward cloned request
B3-->>LB: HTTP Response
LB-->>C: Forward response
Each request is handled by two goroutines:
- forward goroutine — clones the client request (method, headers, body, path) and sends it to the selected backend
- reverse goroutine — copies the backend response (status code, headers, body) back to the client
They communicate over a channel. The main handler blocks until both goroutines finish.
load-balancer/
├── main.go # entry point — configure backends and algorithm here
├── backend/
│ └── main.go # simple test backend server
└── balancer/
├── backend.go # Backend struct with active connection counter
├── balancer.go # core: ServeHTTP, forward/reverse goroutines
├── algorithm.go # Algorithm interface
├── roundrobin.go # Round Robin
├── leastconn.go # Least Connections
├── iphash.go # IP Hash
└── weighted.go # Weighted Round Robin
Rotates through all backends in order, one request at a time.
Request 1 → backend :8081
Request 2 → backend :8082
Request 3 → backend :8083
Request 4 → backend :8081 (wraps around)
Best for: backends with equal capacity and similar request costs.
algo := &balancer.RoundRobin{}Same as Round Robin but each backend gets requests proportional to its weight.
backends: :8081 (weight=1), :8082 (weight=3), :8083 (weight=2)
expanded: [:8081, :8082, :8082, :8082, :8083, :8083]
Request 1 → :8081
Request 2 → :8082
Request 3 → :8082
Request 4 → :8082
Request 5 → :8083
Request 6 → :8083
Request 7 → :8081 (wraps around)
Best for: backends with different hardware capacity — give more traffic to stronger servers.
algo := balancer.NewWeightedRoundRobin(backends)Note: must use
NewWeightedRoundRobin(backends)— not&WeightedRoundRobin{}— so the expanded list is built from the weights before the first request.
Always picks the backend with the fewest active connections at that moment.
:8081 → 5 active connections
:8082 → 2 active connections ← picked
:8083 → 8 active connections
Best for: mixed workloads where some requests take much longer than others (e.g. file uploads alongside simple API calls).
algo := &balancer.LeastConn{}Hashes the client IP to always route the same client to the same backend.
client 192.168.1.10 → hash → :8081 (always)
client 192.168.1.20 → hash → :8083 (always)
Best for: stateful backends where a client must always reach the same server (e.g. session stored in memory).
algo := &balancer.IPHash{}Open three terminals and run one backend per terminal:
go run ./backend/main.go 8081
go run ./backend/main.go 8082
go run ./backend/main.go 8083Edit main.go to set backends and choose an algorithm:
backends := []*balancer.Backend{
balancer.NewBackend("http://localhost:8081", 1),
balancer.NewBackend("http://localhost:8082", 3),
balancer.NewBackend("http://localhost:8083", 2),
}
algo := balancer.NewWeightedRoundRobin(backends)Then start it:
go run ./main.gocurl http://localhost:8080/ping
curl http://localhost:8080/ping
curl http://localhost:8080/pingExpected output (with Weighted Round Robin, weights 1:3:2):
pong from backend :8081
pong from backend :8082
pong from backend :8082
pong from backend :8082
pong from backend :8083
pong from backend :8083
Load balancer logs:
2026/05/26 14:00:01 client=127.0.0.1 method=GET path=/ping → backend=localhost:8081
2026/05/26 14:00:02 client=127.0.0.1 method=GET path=/ping → backend=localhost:8082
2026/05/26 14:00:03 client=127.0.0.1 method=GET path=/ping → backend=localhost:8082
Change the algo line in main.go:
algo := &balancer.RoundRobin{} // Round Robin
algo := balancer.NewWeightedRoundRobin(backends) // Weighted Round Robin
algo := &balancer.LeastConn{} // Least Connections
algo := &balancer.IPHash{} // IP Hash