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 β†’

Free system design + DSA prep. If it helped you crack an interview, consider supporting.