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.

Medium Python 3.9+ Aug 9, 2026 Database scaling & optimization 15 views 0 copies

Python code

55 lines
Python 3.9+
import 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

stdout
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

  1. Use a balanced binary tree or sorted list with custom binary search for faster insert/delete
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Database scaling & optimization

Related tutorials and quizzes for this topic.