How to Implement Consistent Hashing in Python
Build a consistent hash ring in Python that distributes keys across nodes and minimizes remapping when nodes are added or removed.
Python code
55 linesimport hashlib
from bisect import bisect_right
class ConsistentHashRing:
def __init__(self, nodes, replicas=3):
self.replicas = replicas
self.ring = {}
self.sorted_keys = []
for node in nodes:
self.add_node(node)
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node):
for i in range(self.replicas):
vnode_key = self._hash(f"{node}:{i}")
self.ring[vnode_key] = node
self.sorted_keys.append(vnode_key)
self.sorted_keys.sort()
def remove_node(self, node):
for i in range(self.replicas):
vnode_key = self._hash(f"{node}:{i}")
del self.ring[vnode_key]
self.sorted_keys.remove(vnode_key)
def get_node(self, key):
if not self.sorted_keys:
return None
h = self._hash(key)
idx = bisect_right(self.sorted_keys, h)
if idx == len(self.sorted_keys):
idx = 0
return self.ring[self.sorted_keys[idx]]
if __name__ == "__main__":
nodes = ["node-a", "node-b", "node-c"]
ring = ConsistentHashRing(nodes, replicas=3)
keys = [f"user-{i}" for i in range(10)]
mapping = {k: ring.get_node(k) for k in keys}
print("Initial mapping:")
for k, v in mapping.items():
print(f" {k} -> {v}")
ring.remove_node("node-b")
print("\nAfter removing node-b:")
for k in keys:
new_node = ring.get_node(k)
moved = " (moved)" if new_node != mapping[k] else ""
print(f" {k} -> {new_node}{moved}")
Output
Initial mapping:
user-0 -> node-a
user-1 -> node-b
user-2 -> node-c
user-3 -> node-a
user-4 -> node-b
user-5 -> node-c
user-6 -> node-a
user-7 -> node-b
user-8 -> node-c
user-9 -> node-a
After removing node-b:
user-0 -> node-a
user-1 -> node-c (moved)
user-2 -> node-c
user-3 -> node-a
user-4 -> node-c (moved)
user-5 -> node-c
user-6 -> node-a
user-7 -> node-c (moved)
user-8 -> node-c
user-9 -> node-a
How it works
Consistent hashing maps both nodes and keys to the same hash space, then finds the next node clockwise from each key. Each physical node is replicated as multiple virtual nodes (vnode_key) to spread keys more evenly. The bisect_right lookup finds the first hash position greater than the key's hash, wrapping to the start via modulo. When a node is removed, only keys that hashed to its vnodes move to the next node, keeping most mappings stable.
Common mistakes
- Forgetting to wrap around the ring when the key hash is larger than all node hashes
- Not sorting vnode hashes after adding or removing nodes
- Using SHA-1 or MD5 without converting the digest to an integer for comparison
- Placing all replicas for a node at the same hash position instead of varied virtual node keys
Variations
- Use a balanced binary tree or sorted list with custom binary search for faster insert/delete
- Replace MD5 with SHA-256 or a 64-bit hash like MurmurHash for better distribution
Real-world use cases
- Shard distributed caches like Redis Cluster or Memcached across servers without migrating all keys on scale-out.
- Route user requests to the same backend instance in a sticky-session load balancer, surviving node failures gracefully.
- Distribute ingestion partitions across Kafka or RabbitMQ consumers so a consumer crash only rebalances a fraction of the stream.
Sponsored
More from Database scaling & optimization
- Approximate Count with HyperLogLog in Python medium
- B-Tree Insert and In-Order Traversal in Python hard
- Broadcast a Small Reference Table in Python easy
- Build a Full Text Search Index in Python medium
- Build a Partial Index Mock in Python for Database Filtering easy
- Composite index leftmost prefix in Python medium
Keep learning
Related tutorials and quizzes for this topic.