Free — no signup required

Load Balancer Algorithms

3 min read

Distributing the Load

A load balancer sits in front of a pool of servers and decides which server handles each incoming request. The choice of algorithm has significant implications for performance, fairness, and resilience — especially when servers are added or removed.

1. Round Robin

The simplest approach: cycle through servers in order.

class RoundRobinBalancer:
    def __init__(self, servers):
        self.servers = servers
        self.index = 0

    def get_server(self):
        server = self.servers[self.index]
        self.index = (self.index + 1) % len(self.servers)
        return server
  • Pro: Zero overhead, perfectly fair request distribution.
  • Con: Assumes all servers have equal capacity and all requests have equal cost. A single slow request on Server A doesn't prevent it from receiving the next request.

2. Least Connections

Send each new request to the server with the fewest active connections.

class LeastConnectionsBalancer:
    def __init__(self, servers):
        self.connections = {s: 0 for s in servers}

    def get_server(self):
        server = min(self.connections, key=self.connections.get)
        self.connections[server] += 1
        return server

    def release(self, server):
        self.connections[server] -= 1
  • Pro: Naturally adapts to slow requests — a server handling a long-running WebSocket connection won't receive new requests until it frees up.
  • Con: Requires tracking connection state. More complex to implement correctly in a distributed load balancer (requires shared state or gossip).

3. Consistent Hashing (The SRE Favorite)

This is the algorithm that appears most often in SRE interviews because it solves a subtle but critical problem in distributed systems.

The problem with naive hashing:

# Naive approach
server_index = hash(request_key) % num_servers

This works fine until you add or remove a server. When num_servers changes from 3 to 4, the modulo result changes for almost every key. Every cached item is suddenly mapped to the wrong server — a cache stampede that can bring down your origin servers.

The consistent hashing solution:

Map both servers and requests onto a conceptual ring (values 0 to 2³²). To find a request's server, walk clockwise on the ring until you hit a server.

import hashlib
import bisect

class ConsistentHashRing:
    def __init__(self, servers, replicas=100):
        self.replicas = replicas  # Virtual nodes per server
        self.ring = {}            # hash -> server name
        self.sorted_keys = []     # Sorted list of hashes on the ring

        for server in servers:
            self.add_server(server)

    def _hash(self, key):
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

    def add_server(self, server):
        # Add multiple virtual nodes to ensure even distribution
        for i in range(self.replicas):
            key = self._hash(f"{server}:{i}")
            self.ring[key] = server
            bisect.insort(self.sorted_keys, key)

    def remove_server(self, server):
        for i in range(self.replicas):
            key = self._hash(f"{server}:{i}")
            del self.ring[key]
            self.sorted_keys.remove(key)

    def get_server(self, request_key):
        if not self.ring:
            return None
        h = self._hash(request_key)
        # Find the first server hash >= request hash
        idx = bisect.bisect_right(self.sorted_keys, h) % len(self.sorted_keys)
        return self.ring[self.sorted_keys[idx]]

Why virtual nodes (replicas)? With only 3 real servers on the ring, the distribution of requests is uneven — one server might own 60% of the ring by chance. Adding 100 virtual nodes per server (300 points total) distributes ownership much more evenly.

The key benefit: When you add or remove a server, only the requests that were mapped to that server's ring segment need to be remapped. All other requests are unaffected. In a 10-server cluster, adding one server remaps roughly 1/11 of requests — not all of them.

Interview Tip

A classic follow-up after explaining consistent hashing is: "What happens when a server goes down?" The answer: only the requests that were assigned to that server's ring segment get remapped to the next server clockwise. All other client-to-server mappings remain stable. This is why consistent hashing is used in distributed caches (Memcached, Redis Cluster), CDNs, and databases like Cassandra — stability of mapping is critical for cache hit rates.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free