Load Balancing: The Traffic Cop of Distributed Systems
Load balancing is the cornerstone of distributed systems, ensuring no single server bears too much demand and the entire system remains responsive and available. A load balancer sits between clients and servers, acting as the ‘traffic cop’ that intelligently routes requests across a pool of backend servers using various algorithms. Without load balancing, one server becomes a bottleneck; with it, horizontal scaling becomes a viable strategy for handling growth.
What is a Load Balancer?
A load balancer is a network component that distributes incoming traffic across multiple backend servers. Its core responsibility is to accept client requests and decide which server will handle each request. This distribution serves multiple critical purposes: it prevents any single server from being overwhelmed, maximizes throughput by keeping all servers roughly equally utilized, improves response times by distributing the load, and increases overall system availability by removing dependency on any single server.
Load balancers sit at multiple levels in the network stack. A typical architecture places a load balancer (or cluster of them for redundancy) at the edge, in front of a fleet of identical or near-identical application servers. Modern distributed systems often use multiple layers of load balancing: at the edge for geographic distribution, at the datacenter level for server distribution, and sometimes even within services for database or cache routing.
Layer 4 vs. Layer 7 Load Balancing
Load balancers operate at different layers of the OSI model, and the choice fundamentally changes how decisions are made and what information is available.
Layer 4 (Transport Layer) load balancers make routing decisions based solely on network and transport layer data: IP addresses, TCP/UDP ports, and protocol type. A Layer 4 load balancer does not inspect the content of packets—it examines only headers. This approach is extremely fast and lightweight, as it requires minimal CPU overhead. Examples include HAProxy in TCP mode, Linux IPVS (IP Virtual Server), and AWS Network Load Balancer (NLB). Layer 4 is ideal for high-throughput scenarios, real-time applications, non-HTTP protocols, and when you need minimal latency overhead.
Layer 7 (Application Layer) load balancers make routing decisions based on application-level data: HTTP headers, URLs, hostnames, cookies, request methods, and even request bodies. This allows remarkably sophisticated routing—you can route requests to different backend pools based on URL paths, send API requests to one pool and static content to another, or route based on user authentication tokens. The tradeoff is CPU cost: Layer 7 requires deeper packet inspection and full request parsing. Examples include NGINX, HAProxy in HTTP mode, AWS Application Load Balancer (ALB), and Envoy. Layer 7 is the standard for HTTP(S) services where smarter routing provides real value.
The practical decision often comes down to this: if your traffic is HTTP(S) and you want intelligent routing by hostname, path, or headers, use Layer 7. If you need extreme performance, handle multiple protocols, or route by IP/port only, use Layer 4.
Common Algorithms
The algorithm a load balancer uses to select which server handles a request is where the real variety emerges. Different algorithms make different tradeoffs between simplicity, fairness, and performance.
1. Round Robin
Round Robin is the simplest algorithm: requests are distributed to servers in order, cycling through the pool repeatedly. Server 1, Server 2, Server 3, Server 1, Server 2, and so on. It is trivial to implement and requires no state tracking, making it fast and predictable. Round Robin works best when all backend servers have identical capacity and all requests have similar processing time. Problems emerge when servers are heterogeneous (some fast, some slow) or when request costs vary wildly—a slow server still gets the same number of requests as a fast one.
Variants like weighted round robin address this by assigning a weight to each server (a fast server might get weight 2, a slow one weight 1), so faster servers receive more traffic. This works well when the performance characteristics of servers are known and static.
2. Least Connections
Least Connections sends each new request to the server currently handling the fewest active connections. This algorithm is particularly effective when request costs vary widely—short requests and long requests are treated differently by connection count rather than forcing equal distribution. If one slow request ties up a server, the load balancer will route new traffic to less-busy servers, naturally load-balancing around that slowness.
Least Connections requires the load balancer to track active connection counts for each server, adding minimal overhead but introducing state that must be maintained accurately. It works well for connection-pool scenarios (like database connection pooling) and workloads with high variance in request processing time.
3. IP Hash
IP Hash uses the client’s IP address as input to a hash function that maps the client to a specific backend server. The same client IP always maps to the same server. This provides session stickiness—if a user has per-server state (session variables, temporary cache), they will always return to the same server, so their session state remains available.
IP Hash has downsides: it can create uneven load distribution if clients are not uniformly distributed, and it does not adapt to server failures gracefully—a downed server breaks the mapping for all its clients. It is most useful when sessions are server-specific and state cannot be easily shared across the backend pool.
4. Consistent Hashing
Consistent Hashing is crucial for distributed caching and large backend pools where servers frequently come and go. Unlike simple IP hashing where adding a new server invalidates most hash mappings, consistent hashing minimizes the disruption. Servers are placed on a hash ring, and keys (like session IDs) are also placed on the ring; a key is assigned to the next server clockwise on the ring.
When a new server is added, only the keys that fall in the new server’s zone need to be re-mapped; all others stay put. When a server is removed, only its keys need remapping. This property is why consistent hashing is essential for caches (memcached, Redis) and CDNs where backend pools change over time. The downside is complexity—it requires more sophisticated implementation and understanding than simple hashing.
Interactive Visualization
Below is a simulation of a Load Balancer distributing requests to backend servers. You can adjust the algorithm to see how it affects distribution.
Health Checks
A load balancer must only route traffic to healthy servers. A backend server can fail, become overwhelmed, or enter a degraded state where it is alive but not truly ready to serve requests. Without health checks, a load balancer would continue routing traffic to failed servers, degrading the entire system.
Health checks typically work in one of two ways. Passive health checks observe actual client requests: if a backend server closes the connection or times out, the load balancer marks it unhealthy. Active health checks are more common and reliable: the load balancer periodically sends probe requests (HTTP GET to a health endpoint, TCP connection attempts, or custom protocol checks) to each backend server. If the probe succeeds within a timeout, the server is marked healthy; if it fails repeatedly (e.g., 3 failures in a row), the server is marked unhealthy and removed from the pool.
Good health checks are specific to the application. A simple TCP connection check only verifies the server is running, not that it is actually ready to serve requests. Many systems implement a /health or /ready HTTP endpoint that the load balancer can query, allowing the application to report readiness more accurately. When using active health checks, the check frequency and failure threshold are tunable: aggressive checks (every second, 1 failure) detect problems quickly but create traffic overhead; lenient checks (every 10 seconds, 3 failures) are cheaper but introduce latency before a failed server is removed.
Session Stickiness (Sticky Sessions)
Session stickiness ensures that requests from a single client are routed to the same backend server for the duration of a session. This is necessary when the server maintains per-client state that is not shared across the backend pool.
There are several ways to implement stickiness. Source IP stickiness (IP Hash) maps clients by their source IP, but it breaks if clients are behind a NAT or proxy. Cookie-based stickiness is more reliable: the load balancer inserts a cookie in the response (e.g., SERVERID=backend-3) that the client echoes back in subsequent requests, telling the load balancer which server to use. Header-based stickiness works similarly but uses HTTP headers.
The ideal approach is to avoid session stickiness altogether by making sessions distributed and shareable: store session data in a shared cache (Redis) or database so any server can handle a client’s request. This allows the load balancer to route freely without stickiness, scaling more smoothly and recovering better from server failures.
Failover and Redundancy
Load balancers themselves are critical infrastructure and cannot afford a single point of failure. When a load balancer fails, the entire system becomes unavailable. Production systems deploy load balancers in active-active or active-passive configurations.
In active-passive failover, one load balancer is primary and actively handles traffic, while a standby load balancer monitors it. If the primary fails, the standby takes over (often via a virtual IP that switches between them). Failover is clean but introduces a brief moment of unavailability while the switch occurs.
In active-active configurations, multiple load balancers simultaneously handle traffic, often geographically distributed. Clients are directed to them via DNS round-robin or geographic routing. This provides redundancy without a single failover point and can scale load balancing itself across datacenters.
Implementation Considerations
When designing a load balancing strategy, several practical concerns emerge. First is connection pooling and persistence. If backend servers maintain persistent connections to databases or cache servers, you need to manage connection limits. A load balancer with 100 backend servers creates 100 connections to the database; if each server creates its own connection pool, you can exceed database limits quickly.
Second is handling slow clients and response buffering. A backend server may generate a response quickly, but a slow client on a high-latency connection consumes a server resource while that response is being transmitted. Load balancers handle this by buffering responses, so the server completes and returns to the pool while the load balancer manages sending to the slow client.
Third is connection multiplexing in Layer 7. A single client connection to the load balancer might make multiple sequential requests (HTTP/1.1 keep-alive or HTTP/2 multiplexing). The load balancer does not necessarily route all requests from one client connection to the same backend server; it can distribute individual requests. This offers more flexibility than connection-level stickiness but breaks application assumptions that all requests from one client go to the same server.
Fourth is timeout configuration. The load balancer needs timeouts for: connecting to the backend (if it is too long, failing servers hang), receiving a response (slow backends can hang the system), and keep-alive (how long to hold idle connections). These timeouts must be balanced with actual application behavior to avoid prematurely closing legitimate requests.
Finally, monitoring and observability are critical. The load balancer should expose metrics on request rates, response times, error rates, active connections, and per-backend server statistics. Without visibility into load balancer behavior, diagnosing system problems becomes very difficult.
Choosing the Right Algorithm
The decision tree for algorithm selection is straightforward. For stateless or shared-state services where any server can handle any request, use least connections or weighted round robin for simplicity and even distribution. If session state is server-local and must stick to one server, use cookie-based stickiness or make the state distributed. For caching systems or services where membership changes frequently, use consistent hashing. For extreme performance with simple requirements, use Layer 4 with round robin. For HTTP services needing intelligent routing, use Layer 7 with path-based or header-based routing.
The universal principle: design your application to be load-balancer-friendly. Avoid server-local state, make services stateless or use distributed caches, implement proper health checks, and keep request processing time predictable. When load balancing is a design afterthought, you end up fighting it; when it is baked in from the start, scaling becomes natural.
Conclusion
Load balancing is essential for distributing traffic across a pool of servers and building scalable systems. Choose between Layer 4 (fast, simple, network-level) and Layer 7 (smarter, HTTP-aware) based on your needs. Round Robin works for uniform servers; Least Connections handles variable request costs; IP Hash and Consistent Hashing provide session stickiness and cache affinity. Health checks are mandatory to remove failed servers automatically. Session stickiness should be avoided where possible in favor of distributed state. Design services to be load-balancer-friendly: stateless, with good health endpoints, and predictable performance so the load balancer can route traffic effectively.