Ecommet
Topic 31 of 75

Consistent Hashing

Consistent hashing places servers and keys on a ring, so adding or removing a server moves only a small share of the keys. Learn how the ring works, why virtual nodes matter, and where it is used.

Advanced 7 min read 4 quiz questions

Jump to the quiz

In a nutshell

  • Servers and keys are hashed onto the same ring; a key belongs to the first server clockwise from it.
  • Adding or removing one of N servers moves only about 1/N of the keys, versus most keys with hash % N.
  • Virtual nodes (many points per server) spread keys evenly and let stronger servers take more.
  • Used by distributed caches, Dynamo-style databases (Cassandra, DynamoDB) and load balancers.

The ring

Take a hash function whose output is a big number, and bend its range into a circle: 0 at the top, the maximum value just before it. Hash each server name to a point on the circle. Hash each key too. A key is stored on the first server you meet walking clockwise from it.

Four servers on a hash ring

Four servers on a hash ring cart:17 → Server B user:42 → Server C img:9 → Server D user:7 → Server A Server A sits at 20° on the ring. Server B sits at 110° on the ring. Server C sits at 210° on the ring. Server D sits at 290° on the ring. cart:17user:42img:9user:7Server AServer BServer CServer D
Each key walks clockwise to the next server. Server B owns every key between A and B.
Diagram as text
  • cart:17 → Server B
  • user:42 → Server C
  • img:9 → Server D
  • user:7 → Server A
  • Server A sits at 20° on the ring.
  • Server B sits at 110° on the ring.
  • Server C sits at 210° on the ring.
  • Server D sits at 290° on the ring.

Now add Server E between B and C. Only the keys between B and E change owner (they used to walk on to C). Every other key stays exactly where it was.

Adding Server E

Adding Server E cart:17 → Server B user:42 → Server E (moved) img:9 → Server D user:7 → Server A Server A sits at 20° on the ring. Server B sits at 110° on the ring. Server E sits at 155° on the ring. Server C sits at 210° on the ring. Server D sits at 290° on the ring. cart:17user:42img:9user:7Server AServer BServer EnewServer CServer D
Only user:42 moves (highlighted). With hash % N, most keys would have moved.
Diagram as text
  • cart:17 → Server B
  • user:42 → Server E (moved)
  • img:9 → Server D
  • user:7 → Server A
  • Server A sits at 20° on the ring.
  • Server B sits at 110° on the ring.
  • Server E sits at 155° on the ring.
  • Server C sits at 210° on the ring.
  • Server D sits at 290° on the ring.

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

Virtual nodes

With only one point per server, the gaps between servers are random sizes, so one server might own 40% of the ring and another 10%. And when a server leaves, all its keys land on a single neighbour.

The fix is virtual nodes: hash each server many times (A#0, A#1, … A#99) so it appears at 100 scattered points. Each server now owns many small arcs, the shares even out, and a departing server's keys spread across all the others. A machine with twice the capacity simply gets twice as many virtual nodes.

A hash ring with virtual nodes (MD5 for an even spread; any good hash works)

Java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;

class HashRing {
    private final int replicas;
    private final TreeMap<Long, String> ring = new TreeMap<>();

    HashRing(Collection<String> nodes, int replicas) {
        this.replicas = replicas;
        for (String node : nodes) {
            addNode(node);
        }
    }

    static long hash(String s) {
        try {
            byte[] d = MessageDigest.getInstance("MD5").digest(s.getBytes(StandardCharsets.UTF_8));
            return ((long) (d[0] & 0xff) << 24) | ((d[1] & 0xff) << 16) | ((d[2] & 0xff) << 8) | (d[3] & 0xff);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    void addNode(String node) {
        for (int i = 0; i < replicas; i++) {
            ring.put(hash(node + "#" + i), node);
        }
    }

    void removeNode(String node) {
        for (int i = 0; i < replicas; i++) {
            ring.remove(hash(node + "#" + i));
        }
    }

    String nodeFor(String key) {
        Map.Entry<Long, String> e = ring.ceilingEntry(hash(key));
        return (e != null ? e : ring.firstEntry()).getValue(); // wrap around
    }
}
Python
import bisect
import hashlib

class HashRing:
    def __init__(self, nodes, replicas):
        self.replicas = replicas
        self.points = []  # sorted hash positions
        self.owner = {}   # position -> node
        for node in nodes:
            self.add_node(node)

    @staticmethod
    def hash(s):
        return int(hashlib.md5(s.encode()).hexdigest()[:8], 16)

    def add_node(self, node):
        for i in range(self.replicas):
            h = self.hash(f"{node}#{i}")
            self.owner[h] = node
            bisect.insort(self.points, h)

    def remove_node(self, node):
        for i in range(self.replicas):
            h = self.hash(f"{node}#{i}")
            del self.owner[h]
            self.points.remove(h)

    def node_for(self, key):
        i = bisect.bisect_left(self.points, self.hash(key))
        return self.owner[self.points[i % len(self.points)]]  # wrap around

The tests check the three promises: adding E moves only keys to E (about a fifth of them); removing B moves only B's keys; and with 100 virtual nodes each of the four servers gets roughly a quarter.

Where it is used

  • Distributed caches: clients pick a Memcached or Redis node per key, and adding a node only invalidates a small share of the cache.
  • Dynamo-style databases: Amazon's Dynamo paper describes partitioning with a ring and virtual nodes; Cassandra uses the same idea, storing each key on the next few nodes clockwise as replicas.
  • Load balancers and CDNs: send the same client or URL to the same backend so its local cache stays warm.

Common mistakes

  • Using one point per server, which gives very uneven shares.
  • Forgetting the wrap-around: keys after the last point belong to the first server.
  • Using a hash function with poor spread, clustering servers together.
  • Thinking consistent hashing removes all data movement: about 1/N of the keys still move, and they must be copied.

Interview questions

Try answering out loud before you open each one.

Why not use hash(key) % N for a distributed cache?

Because changing N (adding or losing a node) remaps almost every key, so the cache is effectively emptied and the database is flooded. With consistent hashing, adding or removing one of N nodes remaps only about 1/N of the keys.

What are virtual nodes for?

Each physical server is placed at many points on the ring. That evens out the share of keys per server, spreads a failed server's keys over all others instead of one neighbour, and lets you weight servers by capacity by giving them more points.

Check yourself

1. Where does a key go on the ring?
Show the answer

Answer: The first server clockwise from its hash. Walk clockwise from the key's position to the first server.

2. With 10 servers, adding one moves about what share of keys?
Show the answer

Answer: About 1/11. Only the keys in the new server's arcs move, roughly 1/(N+1).

3. Why add virtual nodes?
Show the answer

Answer: To even out load and spread a failed server's keys. Many small arcs per server average out to fair shares.

4. A server is removed from the ring. Which keys move?
Show the answer

Answer: Only that server's keys, to the next servers clockwise. Everyone else keeps their keys.

Further reading

Your notes

Saved in this browser as you type.

Finished this topic?

Mark it read to track your progress, then carry on with the next one.

Next topic