Metadata
| Field | Value |
|---|---|
| Slug | load-balancer |
| Difficulty | Intermediate |
| Estimated Reading Time | 15 min |
| Estimated Coding Time | 30 min |
| Tier | 1 — Core Backend Components |
| Implementation Language | Go |
| SEO Description | Learn how Load Balancers work in system design. Master L4 vs L7 routing, Round Robin vs Least Connections, and build a concurrent HTTP load balancer in Go. |
1. Overview
What problem does it solve?
A single server can only handle so much traffic before it runs out of CPU, memory, or network bandwidth. To handle more traffic, you must scale horizontally by adding more servers.
But if you have 10 identical API servers, how does the client know which one to talk to? A Load Balancer (LB) sits in front of your servers, acts as the single point of contact for clients, and distributes incoming requests evenly across the backend fleet.
What breaks without it?
- Availability: If a server crashes, clients talking to that server will fail. A load balancer detects the crash and routes traffic away from it.
- Scalability: You are hard-capped by the limits of a single machine.
- Security: Backend servers are exposed directly to the public internet.
2. Motivation
The Evolution of Load Balancing
- DNS Round Robin (The 90s): Map one domain (
example.com) to multiple IP addresses in DNS. Problem: DNS caches meant clients kept hitting dead IPs for hours after a server died. - Hardware Load Balancers (The 2000s): Expensive, proprietary physical appliances (F5 Networks, Citrix) sitting in data centers. Extremely fast, but inflexible.
- Software Load Balancers (The 2010s): HAProxy, Nginx. Software running on commodity Linux boxes. Cheaper and programmable.
- Cloud & Proxy Era (Present): AWS ALB/NLB, Envoy Proxy. Load balancers are now distributed systems themselves, capable of advanced L7 routing, rate limiting, and observability.
3. Real-World Usage
| System | Tool | Use Case |
|---|---|---|
| Cloud Providers | AWS ALB/NLB | Managed public-facing load balancing |
| Kubernetes | Kube-Proxy / Ingress | Routing traffic to internal pods |
| Microservices | Envoy Proxy | Service Mesh (sidecar load balancing) |
| Netflix | Ribbon / Envoy | Client-side load balancing |
| Global Routing | Cloudflare / Fastly | Global Anycast load balancing |
4. Prerequisites
| Concept | Block |
|---|---|
| HTTP & TCP protocols | 001 HTTP & TCP/IP Fundamentals |
| Finding servers | 004 DNS & Service Discovery |
5. Visual Explanation
L4 vs L7 Load Balancing
graph TD
Client["Client"] --> LB{"Load Balancer"}
subgraph "Layer 4 (Transport)"
LB -- "TCP Port 80" --> S1["Server 1"]
LB -- "TCP Port 80" --> S2["Server 2"]
end
subgraph "Layer 7 (Application)"
LB -- "GET /images/*" --> S3["Image Servers"]
LB -- "POST /api/*" --> S4["API Servers"]
end
High Availability Architecture (Redundant LBs)
If a load balancer is the single point of entry, what happens if the LB crashes?
graph TD
DNS["DNS (example.com)"] --> |Active IP| LB1["Primary LB"]
DNS -.-> |Standby IP| LB2["Secondary LB"]
LB1 --> S1["Server A"]
LB1 --> S2["Server B"]
LB2 -.-> S1
LB2 -.-> S2
LB1 <-.-> |Heartbeat (Keepalived/VRRP)| LB2
6. Internal Working
6.1 Layer 4 (Network) vs Layer 7 (Application)
| Feature | Layer 4 (L4) | Layer 7 (L7) |
|---|---|---|
| What it inspects | IP Addresses, TCP/UDP Ports | HTTP Headers, URL Paths, Cookies |
| Speed | Extremely fast (Kernel level) | Slower (Requires decrypting TLS & parsing HTTP) |
| Routing capability | Dumb (Send next TCP connection to Server B) | Smart (Send /api to API servers, /video to CDN) |
| AWS Equivalent | Network Load Balancer (NLB) | Application Load Balancer (ALB) |
| Use case | DB clusters, high-throughput raw TCP | Microservices, REST APIs, Web Apps |
6.2 Routing Algorithms
When a request arrives, how does the LB choose which server gets it?
- Round Robin: Server 1, Server 2, Server 3, Server 1, Server 2... (Simple, but ignores server load).
- Weighted Round Robin: Server 1 gets 2x more traffic than Server 2 (Useful when servers have different CPU capacities).
- Least Connections: Send request to the server with the fewest active connections (Best for long-lived requests like WebSockets).
- IP Hash:
hash(Client IP) % N. Ensures the same client always hits the same server (Sticky Sessions).
6.3 Active Health Checks
A load balancer is useless if it sends traffic to a dead server.
The LB constantly pings backend servers (e.g., GET /health every 5 seconds). If a server fails 3 consecutive checks, it is removed from the routing pool. When it passes the check again, it is added back.
7. Implementation
Why Go? Go is the undisputed king of modern network proxies (Traefik, CoreDNS, Caddy are all written in Go). Its built-in ReverseProxy struct and ultra-efficient goroutines make it trivial to build a high-performance L7 load balancer.
/*
007 - L7 HTTP Load Balancer
A concurrent load balancer with Round Robin routing,
Active Health Checks, and HTTP Reverse Proxying.
Run: `go run loadbalancer.go`
*/
package main
import (
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"time"
)
// ── Backend Server Representation ──
type Backend struct {
URL *url.URL
Alive bool
mux sync.RWMutex
ReverseProxy *httputil.ReverseProxy
}
func (b *Backend) SetAlive(alive bool) {
b.mux.Lock()
b.Alive = alive
b.mux.Unlock()
}
func (b *Backend) IsAlive() bool {
b.mux.RLock()
alive := b.Alive
b.mux.RUnlock()
return alive
}
// ── Server Pool (Manages all backends) ──
type ServerPool struct {
backends []*Backend
current uint64 // For Round Robin
}
// NextIndex atomically gets the next backend index (Round Robin)
func (s *ServerPool) NextIndex() int {
// A real implementation would use atomic.AddUint64 for thread safety
s.current++
return int(s.current % uint64(len(s.backends)))
}
// GetNextPeer returns the next available healthy server
func (s *ServerPool) GetNextPeer() *Backend {
// We loop through the backends to find a healthy one
next := s.NextIndex()
l := len(s.backends) + next // start from next and wrap around
for i := next; i < l; i++ {
idx := i % len(s.backends)
if s.backends[idx].IsAlive() {
s.current = uint64(idx)
return s.backends[idx]
}
}
return nil // All servers are dead!
}
// ── Active Health Checking ──
func (s *ServerPool) HealthCheck() {
for {
for _, b := range s.backends {
status := "up"
// Send a quick HTTP GET to the backend
resp, err := http.Get(b.URL.String())
if err != nil || resp.StatusCode != http.StatusOK {
b.SetAlive(false)
status = "down"
} else {
b.SetAlive(true)
}
if resp != nil {
resp.Body.Close()
}
log.Printf("HealthCheck: %s is [%s]\n", b.URL, status)
}
time.Sleep(10 * time.Second)
}
}
// ── Load Balancer Entry Point ──
func lbHandler(w http.ResponseWriter, r *http.Request) {
peer := serverPool.GetNextPeer()
if peer != nil {
log.Printf("Routing '%s' to -> %s", r.URL.Path, peer.URL)
// Transparently forward the request
peer.ReverseProxy.ServeHTTP(w, r)
return
}
http.Error(w, "Service Unavailable: No healthy backends", http.StatusServiceUnavailable)
}
var serverPool ServerPool
func main() {
// Initialize three mock backend URLs
servers := []string{
"http://127.0.0.1:8081",
"http://127.0.0.1:8082",
"http://127.0.0.1:8083",
}
for _, s := range servers {
serverUrl, err := url.Parse(s)
if err != nil {
log.Fatal(err)
}
proxy := httputil.NewSingleHostReverseProxy(serverUrl)
serverPool.backends = append(serverPool.backends, &Backend{
URL: serverUrl,
Alive: true,
ReverseProxy: proxy,
})
}
// Start health checking in the background
go serverPool.HealthCheck()
log.Println("Load Balancer listening on port 8080...")
server := http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(lbHandler),
}
// Start the load balancer
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
Note: To fully test this, you would need to run three simple HTTP servers on ports 8081, 8082, and 8083.
8. Complexity
| Metric | Details |
|---|---|
| Routing Time | O(1) for Round Robin and IP Hash. O(N) for Least Connections (must check all servers). |
| Throughput (L4) | Millions of packets per second. Limited mostly by network interface cards (NIC). |
| Throughput (L7) | 10k - 100k requests per second per LB instance. Slower due to HTTP parsing and TLS termination. |
Scalability Characteristics
If a single Load Balancer caps out at 100k RPS, how do massive companies handle 1M+ RPS? Tiered Load Balancing:
- DNS uses Geo-Routing to send you to a specific datacenter.
- An L4 Hardware LB (or ECMP router) distributes TCP connections to a fleet of L7 Software LBs.
- The L7 Software LBs inspect the HTTP headers and route to the actual application servers.
9. Trade-offs
| Routing Algorithm | Pros | Cons |
|---|---|---|
| Round Robin | Simple, fast, deterministic. | Fails terribly if some requests take 1ms and others take 5s. |
| Least Connections | Perfectly balances load regardless of request duration. | Slight overhead to track connection counts. |
| IP Hashing | Ensures "Sticky Sessions" (user always hits same server). | Bad distribution if one IP (like a corporate NAT) sends 50% of traffic. |
10. Production Evolution
| Feature | This Implementation | Production (Nginx / HAProxy / Envoy) |
|---|---|---|
| TLS Termination | None (HTTP only) | Decrypts HTTPS at the edge so backend servers don't waste CPU on crypto. |
| Connection Pooling | New TCP connection to backend | Maintains a pool of warm TCP connections to backends to eliminate handshake latency. |
| Dynamic Config | Hardcoded array | Hot-reloads backend lists via API or Service Registry (Block 004) without dropping traffic. |
| High Availability | Single Process | Active-Passive pairs using Keepalived (VRRP). If Active dies, Passive takes over the IP address. |
11. Common Bugs
| Bug | What happens | Fix |
|---|---|---|
| No Active Health Checks | LB keeps routing 1/3 of your traffic to a dead server, causing 33% of users to see 502 Bad Gateway. | Implement health checks and evict dead peers. |
| Thundering Herd on Startup | A backend restarts. Before its caches are warm, the LB floods it with traffic, killing it again. | Implement Slow Start: slowly ramp up traffic to newly healthy nodes. |
| X-Forwarded-For Missing | Application servers think every user's IP is the Load Balancer's IP (breaks analytics and rate limiting). | LB must inject X-Forwarded-For: <Client IP> into the HTTP headers before proxying. |
| Sticky Session Imbalance | One backend is sitting at 100% CPU while others are at 10%, because a "power user" IP hashed to that server. | Move state out of the app servers into a Distributed Cache (Block 010), then switch to Round Robin. |
12. Interview Questions
-
What is the difference between an L4 and L7 load balancer? Hint: L4 only sees IPs and Ports (TCP/UDP). L7 decrypts traffic and sees HTTP URLs, headers, and cookies.
-
How does a load balancer prevent a single point of failure? Hint: You deploy them in Active-Passive pairs using a protocol like VRRP. If the primary dies, the secondary instantly takes over the IP address.
-
Why is Round Robin a bad idea for a service where request processing times vary wildly (10ms to 10s)? Hint: Long requests pile up on one server by chance, while other servers sit idle. Use Least Connections instead.
-
Your application relies on users hitting the exact same server so they don't get logged out. How do you configure the LB? Hint: IP Hash routing (Sticky Sessions). (Follow-up: This is a bad architecture. State should be moved to Redis so LBs can route anywhere).
13. Used By (Downstream Blocks)
- 008 API Gateway — Often combined with or placed directly behind an L7 load balancer.
- 016 Consistent Hashing — The advanced routing algorithm used by LBs in distributed databases.
- 034 Service Mesh — Envoy acts as a local L7 load balancer running next to every single microservice.
14. Used In (Case Studies)
| System | LB Strategy |
|---|---|
| TinyURL | L7 LB routes /api to write servers, and /<code> directly to read servers. |
| Netflix | Uses Zuul/Envoy at the edge, and client-side load balancing internally. |
| High-throughput L4 load balancers routing raw TCP WebSocket connections. | |
| YouTube | L7 LBs route video requests directly to CDN nodes based on geolocation headers. |
15. Related Blocks
| Relationship | Block |
|---|---|
| Previous | 004 DNS & Service Discovery |
| Next | 008 Reverse Proxy & API Gateway |
16. Try It Yourself
Exercise 1: Least Connections Routing
Modify the Go implementation. Add an ActiveConnections int32 field to the Backend struct. Increment it before ServeHTTP and decrement it after. Change GetNextPeer() to return the healthy backend with the lowest ActiveConnections instead of Round Robin.
Exercise 2: Add X-Forwarded-For
In the lbHandler, before calling ServeHTTP, modify the r.Header to inject the X-Forwarded-For header using the client's IP address (extracted from r.RemoteAddr).
Website Metadata
| Field | Value |
|---|---|
| Hero Title | Load Balancers |
| Hero Subtitle | How to distribute traffic across thousands of servers, scale horizontally, and survive server crashes. |
| Breadcrumb | System Design → Building Blocks → Load Balancers |
| Sidebar Category | Tier 1 — Core Backend Components |
| Search Keywords | load balancer, round robin, least connections, l4 vs l7, active health checks, sticky sessions, nginx, proxy |
| Internal Links | ← 004 DNS & Service Discovery · → 008 API Gateway |
| Suggested Illustration | A traffic cop standing in front of three toll booths, directing a line of cars evenly to the empty lanes. |
| Suggested Animation | Requests flowing to three servers. One server catches fire. The LB instantly detects it via a red "X" heartbeat and routes all traffic to the remaining two. |