Consistent Hashing - Complete Deep Dive

Prerequisites: Load Balancing, Database Sharding Used in: Key-Value Store, CDN routing, distributed caches, database sharding


The Problem: Why Regular Hashing Breaks

With N servers, the simplest approach is:

server = hash(key) % N

hash("user_123") % 3 = 1 → Server 1
hash("user_456") % 3 = 0 → Server 0
hash("user_789") % 3 = 2 → Server 2

Works fine until you add or remove a server:

N changes from 3 to 4:
hash("user_123") % 4 = 3 → Server 3  (was Server 1!)
hash("user_456") % 4 = 0 → Server 0  (same, lucky)
hash("user_789") % 4 = 1 → Server 1  (was Server 2!)

Almost ALL keys get remapped. If you have a cache cluster, this means nearly 100% cache miss — a “cache stampede” that could crash your database.

Goal of consistent hashing: When adding/removing a server, only remap K/N keys (K = total keys, N = total servers). If you have 1M keys and 10 servers, only ~100K keys move instead of ~900K.


How Consistent Hashing Works

Step 1: The Hash Ring

Imagine the hash space as a circle (ring) from 0 to 2^32.

        0
       / \
    359   1
    /       \
  358   ...   2
   |           |
  ...         ...
   |           |
  180         90
     \       /
      \     /
       179
       
(Positions 0 to 2^32 arranged in a circle)

Step 2: Place Servers on the Ring

Hash each server name to get its position on the ring.

hash("Server_A") = 100    → position 100
hash("Server_B") = 200    → position 200
hash("Server_C") = 350    → position 350

Ring:
        0
      /    \
    350(C)   100(A)
   /           \
  ...         200(B)

Step 3: Place Keys on the Ring

Hash each key to get its position. Walk CLOCKWISE to find the first server.

hash("user_1") = 50   → walk clockwise → hits Server_A (at 100)
hash("user_2") = 150  → walk clockwise → hits Server_B (at 200)
hash("user_3") = 250  → walk clockwise → hits Server_C (at 350)
hash("user_4") = 360  → walk clockwise → hits Server_A (at 100, wraps around)

Step 4: Adding a Server

Add Server_D at position 250:

Before: hash("user_3") = 250 → Server_C (at 350)
After:  hash("user_3") = 250 → Server_D (at 250) ← only this key moves!

Only keys between the new server and its predecessor need to move. Everything else stays in place.

Step 5: Removing a Server

Remove Server_B (at 200):

Before: hash("user_2") = 150 → Server_B (at 200)
After:  hash("user_2") = 150 → Server_C (at 350) ← walks to next server

Only keys that were on Server_B move to the next server clockwise.


The Problem: Uneven Distribution

With only 3 servers on the ring, the distribution is often uneven. One server might own 60% of the ring while another owns 10%.

        0
      /    \
    350(C)   10(A)
   /           \
  ...         300(B)

Server A owns: 10 to 300 (290 units of range) → ~80% of traffic!
Server B owns: 300 to 350 (50 units) → ~14%
Server C owns: 350 to 10 (20 units) → ~6%

Virtual Nodes (The Fix)

Instead of placing each server once on the ring, place it MULTIPLE times (virtual nodes).

Server A → hash("A_1")=10, hash("A_2")=120, hash("A_3")=250
Server B → hash("B_1")=50, hash("B_2")=180, hash("B_3")=300
Server C → hash("C_1")=80, hash("C_2")=200, hash("C_3")=350

Ring:
  0→10(A)→50(B)→80(C)→120(A)→180(B)→200(C)→250(A)→300(B)→350(C)

Now each server appears 3 times, and the ranges are much more even.

In practice: Use 100-200 virtual nodes per server. The more virtual nodes, the more even the distribution.

Bonus: When a server is removed, its load is distributed evenly among ALL remaining servers (not just the one neighbor).


When Adding/Removing Servers

Add Server D (with 3 virtual nodes at positions 30, 170, 280):
  - Keys in range (10,30] move from Server B to Server D
  - Keys in range (120,170] move from Server B to Server D
  - Keys in range (250,280] move from Server B to Server D
  
Total keys moved: only those in D's ranges (~1/N of total)

Compare to mod hashing: ~N/(N+1) keys move (nearly everything). Consistent hashing: ~1/N keys move.


Implementation

public class ConsistentHash<T> {
    private final TreeMap<Integer, T> ring = new TreeMap<>();
    private final int virtualNodes;

    public ConsistentHash(int virtualNodes) {
        this.virtualNodes = virtualNodes;
    }

    public void addNode(T node) {
        for (int i = 0; i < virtualNodes; i++) {
            int hash = hash(node.toString() + "_" + i);
            ring.put(hash, node);
        }
    }

    public void removeNode(T node) {
        for (int i = 0; i < virtualNodes; i++) {
            int hash = hash(node.toString() + "_" + i);
            ring.remove(hash);
        }
    }

    public T getNode(String key) {
        if (ring.isEmpty()) return null;
        int hash = hash(key);
        // Find first server clockwise (ceiling)
        Map.Entry<Integer, T> entry = ring.ceilingEntry(hash);
        if (entry == null) {
            // Wrap around to first server
            entry = ring.firstEntry();
        }
        return entry.getValue();
    }

    private int hash(String key) {
        // Use MD5 or MurmurHash for good distribution
        return Math.abs(key.hashCode()) % 360;
    }
}

Real-World Usage

System How it uses consistent hashing
DynamoDB Partitions data across nodes. Adding node = split one partition.
Cassandra Token ring. Each node owns a range. Virtual nodes distribute evenly.
Redis Cluster 16384 hash slots distributed across nodes.
CDN (Akamai) Routes requests to nearest cache node. Adding edge server = minimal cache disruption.
Amazon S3 Internal partition routing for objects.
Discord Routes guilds to specific servers.
Memcached Client-side consistent hashing for cache distribution.

Replication with Consistent Hashing

For fault tolerance, each key is stored on N consecutive nodes (clockwise).

Replication factor = 3

Key at position 150:
  Primary: Server at 180 (first clockwise)
  Replica 1: Server at 200 (second clockwise)
  Replica 2: Server at 250 (third clockwise)

If primary dies, replicas still have the data. New primary is automatically the next node.


Common Interview Questions

Q: “Why not just use mod hashing?” A: Mod hashing remaps ~100% of keys when you add/remove a server. Consistent hashing only remaps ~1/N keys. This is critical for caches and distributed databases where remapping means cache misses or data migration.

Q: “What are virtual nodes?” A: Each physical server is placed multiple times on the hash ring (100-200 positions). This ensures even distribution and smooth load transfer when nodes join/leave.

Q: “How does DynamoDB handle hot partitions?” A: If one partition gets too hot, it splits into two (each taking half the key range). Consistent hashing ensures only the affected range is split — other partitions are undisturbed.

Q: “How many virtual nodes should I use?” A: More virtual nodes = more even distribution. 100-200 per node is standard. Beyond that, diminishing returns and more memory for the ring.

Q: “What hash function should I use?” A: MurmurHash3 or xxHash (fast, good distribution). NOT Java’s hashCode() (poor distribution, clusters). NOT MD5/SHA (too slow for this use case).


Consistent Hashing vs Rendezvous Hashing

  Consistent Hashing Rendezvous (Highest Random Weight)
How Hash ring with virtual nodes Hash each (key, server) pair, pick highest
Add/Remove ~K/N keys move ~K/N keys move
Complexity O(log N) lookup (TreeMap) O(N) lookup (check all servers)
Memory Stores ring Stores server list only
When Large N (many servers) Small N (< 20 servers)

← Back to Fundamentals ← CAP Theorem Next: Database Indexing →

💬 Comments