Skip to content
Insight CodeAcademy

Consistent Hashing Explained

In this article we will learn what consistent hashing is, why the simple modulo approach falls apart when you add a server, how the hash ring works step by step, what virtual nodes solve, and where consistent hashing is used in real systems like Cassandra, DynamoDB and Memcached.

Naveen Garg7 min read
Consistent Hashing Explained

Consistent hashing is the technique that lets a distributed cache or database add and remove servers without reshuffling nearly all of its data. It appears in Cassandra, DynamoDB, Memcached clients, Riak and most CDN routing layers, and it shows up in system design interviews whenever sharding is discussed.

In this article we will learn what consistent hashing is, why the obvious modulo approach falls apart the moment you add a server, how the hash ring works step by step, what problem virtual nodes solve, and where the technique is used in practice.

What is consistent hashing?

Short answer

Consistent hashing is a technique for distributing keys across a changing set of servers so that adding or removing one server only moves the keys that belonged to it, rather than remapping almost every key. It maps both servers and keys onto the same circular hash space, and each key is owned by the first server encountered moving clockwise from the key's position.

Why not just use modulo?

The obvious way to spread keys over n servers is:

server_index = hash(key) % number_of_servers

With four servers this works fine. Now add a fifth.

# Before: hash(key) % 4
# After:  hash(key) % 5

Every key whose hash modulo 4 differed from its hash modulo 5 now maps to a different server — which is roughly 80% of all keys. In a cache, that means an 80% miss rate and a stampede of traffic onto your database at exactly the moment you were trying to add capacity. In a database, it means moving 80% of your data across the network.

Servers before → afterFraction of keys remapped (modulo)Fraction remapped (consistent hashing)
4 → 5~80%~20%
10 → 11~91%~9%
100 → 101~99%~1%

The pattern is brutal: modulo remaps (n-1)/n of keys, while consistent hashing remaps only 1/n.

Keys 0–19 before and after adding a fifth server. With modulo, 16 of 20 keys land on a different server.

How does the hash ring work?

Imagine the output range of your hash function — say 0 to 2³² − 1 — bent into a circle so the largest value sits next to zero.

Each key belongs to the first server clockwise from it. When D joins at 180, only order:5 — the key between B and D — changes owner.

Three steps:

  1. Place the servers. Hash each server's identifier and place it at that position on the ring.
  2. Place the keys. Hash each key the same way to get its position on the ring.
  3. Assign ownership. Walk clockwise from the key's position; the first server you meet owns that key.

Now add a fourth server, D, landing at position 180. Only keys sitting between B at 120 and D at 180 change hands — they used to walk clockwise to C, and now they stop at D. Every other key on the ring is untouched.

Removing a server is the mirror image: its keys walk clockwise to the next server, and nothing else moves.

Why do we need virtual nodes?

The basic ring has a problem. With only a handful of servers hashed onto it, their positions are unlikely to be evenly spaced. One server can end up responsible for a 40% arc of the ring while another owns 10%.

Worse, when a server is removed, its entire arc transfers to a single neighbour, which can double that neighbour's load and cascade into a failure.

Virtual nodes fix both. Instead of hashing each server once, hash it many times with different suffixes:

def build_ring(servers, replicas=150):
    ring = {}
    for server in servers:
        for i in range(replicas):
            position = hash_fn(f"{server}#{i}")
            ring[position] = server
    return dict(sorted(ring.items()))

Each physical server now occupies 150 scattered positions around the ring. Two things improve:

  • Distribution evens out. With 150 virtual nodes per server, the law of large numbers takes over and each server's total share converges close to its fair fraction.
  • Removal spreads out. When a server leaves, its 150 arcs are inherited by many different neighbours rather than one, so no single server absorbs the whole load.
Same three servers, same ring. Scattering each server across several positions evens out how much of the ring each one owns.

Virtual nodes also make heterogeneous hardware straightforward. Give a server with twice the memory twice as many virtual nodes and it receives roughly twice the keys, with no other changes.

How is a key looked up?

Store the sorted virtual node positions in an array and binary search it:

import bisect
 
def get_server(ring_positions, ring, key):
    position = hash_fn(key)
    index = bisect.bisect_right(ring_positions, position)
    if index == len(ring_positions):
        index = 0                       # wrap around the circle
    return ring[ring_positions[index]]

That is O(log n) per lookup where n is the number of virtual nodes — a few microseconds even with thousands of them.

Where is it used?

SystemHow it uses consistent hashing
Amazon DynamoDBPartitions keys across storage nodes; the original Dynamo paper popularised virtual nodes
Apache CassandraToken ring assigns data ranges to nodes
Memcached clientsClient-side ring so cache nodes can be added without a full flush
RiakPartition ring with configurable replication
Akamai / CDN routingMaps content to edge servers
DiscordRoutes guilds to session servers

Most of these systems also replicate. Each key is stored on the next N distinct physical servers clockwise, skipping any virtual node that belongs to a machine already holding a copy:

Replication factor 3. The second B is skipped because it is the same machine — a copy there would not survive B failing.

Common mistakes

  • Confusing it with sharding in general. Range-based and directory-based sharding are also valid; consistent hashing is one strategy with a specific benefit.
  • Using too few virtual nodes. Below about 100 per server, imbalance is visible in production.
  • Assuming it solves hot keys. It balances key distribution, not per-key popularity.
  • Forgetting replication. Real systems store each key on the next N distinct physical servers clockwise, not just the first one, so a node failure does not lose data.
  • Using a weak hash function. Clustered hash output defeats the whole mechanism. MD5 or MurmurHash are the usual choices — cryptographic strength is not required, but good distribution is.

Conclusion

Consistent hashing maps servers and keys into the same circular hash space and assigns each key to the first server clockwise from it. Adding or removing a server moves only that server's share of keys — roughly 1/n — instead of the (n-1)/n that modulo hashing would remap.

Virtual nodes, typically 100 to 200 per physical server, are what make it work in practice: they even out the distribution and spread a failed server's load across many neighbours rather than one.

It is the default answer whenever a design interview reaches the question "what happens when you add a server?"

Frequently asked questions

How many virtual nodes should each physical server have?
Typically between 100 and 200 per physical server. Fewer than about 100 leaves noticeable load imbalance; far more increases the memory and lookup cost of the ring without meaningfully improving distribution. Dynamo used 100 to 200 and most systems have settled near that range.
Does consistent hashing eliminate hotspots completely?
No. It evens out the distribution of keys across servers, but it cannot help if one individual key is extremely popular — that key still lands on one server. Celebrity keys need a separate remedy such as replication of that key or a local cache in front of the ring.

Read next

System Design7 min read

SQL vs NoSQL: How to Actually Choose

In this article we will learn what genuinely separates SQL and NoSQL databases, why the common answers about scale and schema are mostly wrong, the four NoSQL families and what each is for, and a decision procedure you can defend in a system design interview.

DSA6 min read

BFS vs DFS: Which Traversal Should You Use?

In this article we will learn how breadth-first search and depth-first search actually differ, why BFS finds the shortest path in an unweighted graph and DFS does not, how their memory usage compares on wide and deep graphs, and a decision rule for choosing between them in an interview.

DSA7 min read

The Sliding Window Pattern

In this article we will learn what the sliding window pattern is, how it turns an O(n²) brute force into an O(n) scan, the difference between fixed-size and variable-size windows, how to recognise a sliding window problem from its wording, and the mistakes that quietly break the invariant.