Welcome to your sixth lesson in the System Design: Scalability, Caching, and Storage module. In our last lesson, we explored the CAP Theorem, which forced us to choose between Consistency and Availability in the face of network partitions. This led us to design systems with multiple servers to ensure resilience and availability.
Now that we have a fleet of servers, we face a new problem: when a request arrives, how do we decide which server should handle it? Simply sending all traffic to one server defeats the purpose of having a distributed system. Today, we'll design the "traffic cop" of our architecture: the load balancer. You will learn to design load balancing strategies using core algorithms like Round-Robin and Least Connections, and we'll analyze the critical trade-offs that engineers at companies like Netflix and Google consider every day.
1. The Core Concept: What is a Load Balancer?
A load balancer is a device or software that acts as a reverse proxy, sitting in front of your application servers and distributing incoming client requests across the entire fleet of servers. Its primary goal is to ensure no single server becomes overwhelmed, which leads to several key benefits: high availability, horizontal scalability, and improved application responsiveness.
What is a LOAD BALANCER really about?
To begin, let's get a high-level overview of what a load balancer is and the value it provides in a distributed system. This short clip from ByteByteGo provides an excellent foundation.
Watch the first 1 minute and 12 seconds of the video. Focus on the four main benefits a load balancer provides: distributing workload, enabling dynamic scaling, reducing latency, and enhancing availability.
-
Technical Explanation: A load balancer receives incoming network traffic destined for a single IP address (a "virtual IP") and forwards it to one of several backend servers. By doing so, it abstracts the pool of servers into a single, more resilient service. There are two main types you'll encounter:
- Layer 4 (L4) Load Balancers: Operate at the transport layer (TCP/UDP). They make routing decisions based on IP addresses and port numbers. They are extremely fast but have no knowledge of the application-level data (e.g., HTTP headers).
- Layer 7 (L7) Load Balancers: Operate at the application layer (HTTP/HTTPS). They can inspect the content of the request, such as URLs, headers, and cookies, allowing for much more intelligent routing decisions. Most web application load balancing happens at L7.
-
Metaphor for a 5-Year-Old: Imagine a popular ice cream shop with only one person serving. The line gets very long! The owner then hires three more people and puts a host at the front door. The host (the load balancer) looks at the four servers and directs the next customer to the one who is free. This way, the line moves much faster, and if one server needs a break, the host simply stops sending customers to them.
-
Python Implementation: Let's create a basic
LoadBalancerclass in Python. We'll build upon this skeleton as we explore different algorithms.import random class LoadBalancer: def __init__(self, servers): """ Initializes the load balancer with a list of server addresses. :param servers: A list of strings, e.g., ["http://server1", "http://server2"] """ if not servers: raise ValueError("Server list cannot be empty") self.servers = servers def get_next_server(self): """ This method will be implemented by specific algorithms. For now, it's an abstract placeholder. """ raise NotImplementedError("Subclasses must implement this method") def __str__(self): return f"{self.__class__.__name__}(servers={self.servers})" # We cannot instantiate the base class directly, but we will subclass it shortly.
2. Static Algorithm: Round-Robin
Round-Robin is the simplest and most common starting point for load balancing. It distributes requests to servers in a fixed, sequential order.
-
Technical Explanation: The load balancer maintains an internal counter. The first request goes to server 1, the second to server 2, and so on, until it reaches the last server. The next request then "wraps around" and goes back to server 1. The selection logic is
next_server_index = (last_index + 1) % number_of_servers. This is a static algorithm because its decision-making process doesn't depend on the current state of the servers. -
Metaphor for a 5-Year-Old: It's like dealing cards to a group of friends. You give one card to the first friend, one to the second, one to the third, and then start over with the first friend again. Everyone gets a turn in the exact same order, every time.
-
Python Implementation:
class RoundRobinLoadBalancer(LoadBalancer): def __init__(self, servers): super().__init__(servers) self.current_index = -1 # Start at -1 to make the first server index 0 def get_next_server(self): """ Returns the next server in a sequential, circular order. """ self.current_index = (self.current_index + 1) % len(self.servers) return self.servers[self.current_index] # Example Usage servers = [f"Server_{i}" for i in range(1, 4)] # ["Server_1", "Server_2", "Server_3"] rr_lb = RoundRobinLoadBalancer(servers) print("Round-Robin distribution:") for i in range(6): print(f"Request {i+1} -> {rr_lb.get_next_server()}") # Expected Output: Server_1, Server_2, Server_3, Server_1, Server_2, Server_3 -
Design Decisions (Pros & Cons):
- Pros: Extremely simple to implement, very low computational overhead, and inherently stateless (each load balancer instance can run independently).
- Cons: It's "blind." It assumes all servers are identical and equally capable. If one server is more powerful than others, its capacity will be underutilized. More importantly, if one server is slow or overloaded, Round-Robin will continue to send traffic to it, potentially worsening the problem.
3. Static Algorithm: Weighted Round-Robin
Weighted Round-Robin (WRR) improves on basic Round-Robin by accounting for differences in server capacity.
-
Technical Explanation: In WRR, each server is assigned a static "weight," typically an integer. A server with a higher weight will receive proportionally more requests. For example, if Server A has a weight of 3 and Server B has a weight of 1, for every 4 requests, Server A will receive 3 and Server B will receive 1. While this is still a static algorithm, it allows for more efficient use of a heterogeneous server fleet.
-
Metaphor for a 5-Year-Old: Imagine you're sharing candy with your friends. Your best friend (a powerful server) gets 3 pieces of candy for every 1 piece you give to your other friend (a less powerful server). You still cycle through them, but the best friend gets more candy in each cycle.
-
Python Implementation: A common and effective implementation of WRR is the "smooth" weighted round-robin used by Nginx. It avoids sending bursts of requests to high-weight servers.
class WeightedRoundRobinLoadBalancer(LoadBalancer): def __init__(self, servers_with_weights): # servers_with_weights is a dict like {"Server_1": 5, "Server_2": 2} if not servers_with_weights: raise ValueError("Server dictionary cannot be empty") self.servers = list(servers_with_weights.keys()) self.weights = list(servers_with_weights.values()) self.current_weights = [0] * len(self.servers) self.total_weight = sum(self.weights) def get_next_server(self): """ Implements a smooth weighted round-robin algorithm. """ # On each turn, increment all current weights by their static weight for i in range(len(self.servers)): self.current_weights[i] += self.weights[i] # Select the server with the highest current weight best_server_index = self.current_weights.index(max(self.current_weights)) # Decrement the selected server's current weight by the total weight self.current_weights[best_server_index] -= self.total_weight return self.servers[best_server_index] # Example Usage servers_w = {"Server_1": 5, "Server_2": 1, "Server_3": 1} wrr_lb = WeightedRoundRobinLoadBalancer(servers_w) print("\nWeighted Round-Robin distribution:") distribution = {} for i in range(7): server = wrr_lb.get_next_server() print(f"Request {i+1} -> {server}") distribution[server] = distribution.get(server, 0) + 1 print(f"Final Distribution: {distribution}") # Expected Output: Server_1 should receive 5 requests, Server_2 and Server_3 one each. -
Design Decisions (Pros & Cons):
- Pros: Efficiently utilizes servers with different capacities. Still stateless and relatively simple.
- Cons: The weights are static. It cannot adapt to transient changes in server load, such as a server experiencing a temporary garbage collection pause or a sudden spike in CPU usage.
4. Dynamic Algorithm: Least Connections
This is where we move into dynamic algorithms that make decisions based on the current state of the system. The Least Connections algorithm sends the next request to the server with the fewest active connections.
-
Technical Explanation: The load balancer maintains a counter for the number of active connections for each backend server. When a new request arrives, it simply picks the server with the lowest counter value. This strategy is also known as "Join the Shortest Queue" and is highly effective when requests have varying completion times.
-
Metaphor for a 5-Year-Old: You're at the grocery store and need to check out. You don't just pick a random checkout lane; you look at all the lanes and go to the one with the shortest line. This is exactly what the Least Connections algorithm does.

-
Python Implementation: To implement this, we need to simulate connections being opened and closed.
class LeastConnectionsLoadBalancer(LoadBalancer): def __init__(self, servers): super().__init__(servers) # Track active connections for each server self.connections = {server: 0 for server in self.servers} def get_next_server(self): """ Returns the server with the fewest active connections. """ # Find the server with the minimum number of connections best_server = min(self.connections, key=self.connections.get) return best_server def open_connection(self, server): self.connections[server] += 1 print(f"Connection opened to {server}. Current state: {self.connections}") def close_connection(self, server): self.connections[server] -= 1 print(f"Connection closed from {server}. Current state: {self.connections}") # Example Usage lc_lb = LeastConnectionsLoadBalancer(servers) print("\nLeast Connections distribution:") # Simulate some initial traffic lc_lb.open_connection("Server_1") lc_lb.open_connection("Server_1") lc_lb.open_connection("Server_2") # New request arrives, where should it go? next_server = lc_lb.get_next_server() print(f"Next request goes to -> {next_server}") # Expected: Server_3 lc_lb.open_connection(next_server) next_server = lc_lb.get_next_server() print(f"Next request goes to -> {next_server}") # Expected: Server_2 lc_lb.open_connection(next_server) # A connection to Server_1 finishes lc_lb.close_connection("Server_1") next_server = lc_lb.get_next_server() print(f"Next request goes to -> {next_server}") # Expected: Server_1 -
Design Decisions (Pros & Cons):
- Pros: Adapts to real-time server load and varying request durations. Naturally sends less traffic to slow or overloaded servers.
- Cons: Requires state management (the connection counts), which adds complexity. In a large distributed load balancer cluster, each instance only knows about its own connections, which can lead to a "herding" problem where all LBs simultaneously pick the same "least connected" server and overload it.
5. Advanced Strategies & Real-World Application at Netflix
Elite engineering teams rarely use simple algorithms out-of-the-box. They combine and adapt them to solve for the deficiencies we've discussed.
Load Balancing Algorithms & Rethinking Netflix's Edge Load Balancing
To understand the limits of basic algorithms and explore more advanced solutions, let's dive into the core algorithms in detail and then see how Netflix evolved its load balancing.
First, read the article from AlgoMaster to solidify your understanding. Read the entire section '6.1 Load Balancing Algorithms', which covers Round Robin, Weighted Round Robin, Least Connections, and more. Pay close attention to the summary table at the end.
Now, let's look at two advanced concepts and see how Netflix applies them.
-
Technical Explanation:
- Weighted Least Connections: A hybrid approach that combines the ideas from WRR and Least Connections. The load balancer selects the server with the lowest ratio of
active_connections / weight. This is often the best general-purpose choice, as it balances both static capacity and real-time load. - Power of Two Choices: A simple, powerful, and randomized algorithm that elegantly mitigates the "herding" problem of Least Connections without requiring a shared state. The logic is:
- Randomly pick two servers from the pool.
- Compare their number of active connections.
- Send the request to the server with fewer connections.
This small change dramatically improves load distribution over pure random selection and avoids the need for global knowledge of all connection counts.
- Weighted Least Connections: A hybrid approach that combines the ideas from WRR and Least Connections. The load balancer selects the server with the lowest ratio of
-
Metaphor for a 5-Year-Old (Power of Two Choices): You're back at the grocery store. Instead of looking at all the checkout lines (Least Connections), which takes time, you just glance at two random lines and pick the shorter of those two. It's almost as good as checking every line but much faster.
-
Case Study: Netflix's Load Balancing Evolution
Netflix initially used Round-Robin but found it caused overload errors, especially when servers were just starting up or experiencing temporary slowdowns. They moved to a more sophisticated system based on the Choice-of-2 algorithm (the same idea as Power of Two Choices).
Rethinking Netflix's Edge Load Balancing
Let's read directly from the Netflix Technology Blog about why they moved away from Round-Robin and what they built instead.
Read the sections 'Background', 'Load-Balancing Approach', 'Choice-of-2 Algorithm Instead of Round-Robin', and 'Choose based on Combination of Factors'. Focus on why simple Round-Robin and distributed Join-the-Shortest-Queue (Least Connections) were insufficient and how their 'Choice-of-2' approach combined with server-reported health provided a much more resilient solution.
Netflix's solution is a masterclass in system design:
* It uses **Choice-of-2** to avoid herding and distribute load effectively.
* It combines the load balancer's view (local connection count) with the **server's own view** of its utilization, which the server sends back in an HTTP response header.
* This multi-factor approach makes the system robust against various failure modes.
Test your understanding! (Interview-Style Question)
You are designing the backend for a video-sharing platform. There are two primary types of requests:
- Metadata requests: Quick API calls to fetch video titles, descriptions, and comments. These take ~50ms.
- Transcoding jobs: CPU-intensive tasks where an uploaded video is converted into different resolutions (1080p, 720p, etc.). These can take several minutes.
Your server fleet is heterogeneous, with some new powerful machines and some older, less powerful ones. Which load balancing algorithm would you choose and why?
Show answer
This is a classic trade-off question. There is no single perfect answer, but the best answer justifies the choice based on the workload.
A Weighted Least Connections algorithm would be the most suitable choice here.
-
Why not Round-Robin or Weighted Round-Robin? These static algorithms are a poor fit because of the transcoding jobs. A server might be assigned a new transcoding job and be busy for several minutes. A "blind" algorithm like Round-Robin would continue to send it quick metadata requests, which would likely time out, leading to errors.
-
Why not simple Least Connections? This is a good choice, but it doesn't account for the heterogeneous server fleet. A less powerful machine might be able to handle 50 concurrent metadata requests, while a powerful machine could handle 200. Simple Least Connections would treat them equally, leading to the less powerful machines becoming overloaded more quickly.
-
Why Weighted Least Connections is a strong choice: It solves both problems.
- The weight component allows you to assign higher weights to your more powerful servers, so they are allocated a proportionally larger share of the traffic.
- The least connections component provides dynamic adaptation. When a powerful server picks up a long-running transcoding job, its active connection count will remain elevated. The algorithm will naturally divert both new transcoding jobs and quick metadata requests away from it until it finishes, preventing it from being overwhelmed.
This strategy ensures that both long and short jobs are distributed intelligently, taking into account both the static capacity of the servers and their real-time load.
Conclusion
You have now moved from understanding why we distribute systems to how we manage traffic across them. We've seen that the choice of a load balancing algorithm is a critical design decision with direct impact on performance, reliability, and cost-efficiency.
Key Takeaways:
- Load balancers are essential for creating scalable and highly available systems.
- Static algorithms (Round-Robin, WRR) are simple and fast but are blind to the real-time health and load of servers.
- Dynamic algorithms (Least Connections) adapt to server load but introduce state and complexity.
- Advanced strategies like Weighted Least Connections and Power of Two Choices offer sophisticated solutions that balance these trade-offs.
- Real-world systems at companies like Netflix use custom, multi-factor approaches that combine client-side and server-side metrics for maximum resilience.
Next Lesson Preview
A load balancer is only as good as the servers it's sending traffic to. If it directs requests to a server that has crashed, the user still sees an error. To prevent this, load balancers constantly perform health checks. But how do servers store the actual data? Now that we can reliably route traffic to our application logic, our next step is to explore the persistence layer. In the next lesson, we will dive into the heart of our backend: databases, comparing the foundational trade-offs between SQL and NoSQL systems.