TL;DR

1. The problem: why hash(key) mod N breaks when N changes

Instagram's feed cache spreads hundreds of millions of keys (timelines, counters, profile objects) across many Redis/Memcached servers. Each app server must decide, with no coordination, which cache server holds a key. The obvious answer:

server_index = hash(key) mod N        # N = number of cache servers

It's fast, stateless and — with a good hash — spreads keys evenly. The problem is what happens when N changes: a server crashes (N decreases) or you add capacity before a big event (N increases). Almost every key's mod N result changes, so almost every key is now "on the wrong server".

1.1 Worked example: 12 keys, 4 → 5 servers

Take keys whose hashes are 0–11 for simplicity:

hash(key)01234567891011
mod 4012301230123
mod 5012340123401
Moved?––––✘✘✘✘✘✘✘✘

8 of 12 keys moved (67%) here. In general a key stays put only if h mod 4 = h mod 5, which is true for 4 out of every 20 consecutive hash values — so over many keys, 80% move. The general rule for going from N to N+1 servers with mod hashing: only about 1/(N+1) of keys stay; the fraction that moves is N/(N+1). The bigger the cluster, the worse it gets: going from 100 to 101 servers moves ~99% of keys — to add 1% capacity!

Assume Instagram's cache tier: 100 M hot keys on 4 servers, 95% hit ratio, 1 M reads/s.
Add a 5th server with mod-N routing:
  80 M keys are now looked up on the "wrong" server → miss
  hit ratio collapses from 95% to roughly 0.95 × 20% ≈ 19%
  database reads jump from 50,000/s (5% misses) to ~810,000/s  → the database falls over.

Removing a failed server (4 → 3) is just as bad: a key stays only if h mod 4 = h mod 3,
true for 3 of every 12 values → 75% of keys move, at exactly the moment you're already degraded.

For a database (rather than a cache), "moving" means physically copying 80% of the data across the network while serving traffic. We need a mapping where adding or removing one server moves only the keys that must move — ideally just 1/N of them. That's what consistent hashing provides (Karger et al., 1997, originally designed for distributed web caching — the work that led to Akamai's CDN).

🧠 Analogy: a round table of hosts Picture a big round table with numbered seats 0–359. A few hosts sit at fixed seats (say 20, 110, 200, 290). Each arriving guest is assigned a seat number by a lottery and then walks clockwise to the first host they meet — that host serves them. If a new host sits down at seat 150, only the guests between seats 110 and 150 switch to the new host; everyone else keeps their host. If a host leaves, only their guests walk on to the next host. Compare "guest number mod number of hosts": one new host and almost everyone is reassigned.

2. The hash ring

Take a hash function that outputs, say, a 32-bit number: 0 to 232 − 1 (≈ 4.3 billion). Now imagine bending that number line into a circle, so that after 232 − 1 comes 0 again. This is the hash ring (Cassandra's Murmur3 ring uses the 64-bit range −263 … 263 − 1).

  1. Place the nodes. Hash each server's identifier (e.g. hash("cache-a") or its IP) to get its position on the ring. Each node's position is called its token.
  2. Place the keys. Hash each key (hash("user:42:timeline")) with the same function to get its position.
  3. Assign ownership. Each key belongs to the first node at or after its position, moving clockwise — the key's successor. Equivalently, each node owns the arc of the ring from the previous node's position (exclusive) up to its own position (inclusive).
                       0 / 2^32
                  .-----  ●A (pos 20) -----.
             k4 •'                          `• k1  ──► owned by B
           ●D  /                              \
        (290)  |      ring positions          |
   k3 • ──► D  |      increase clockwise      •  k5 ──► B
               \                              /
                `.                          .● B (pos 110)
                  `-----  ●C (200) -----• k2 ──► C
 Rule: walk clockwise from the key's position; the first node you meet owns it.
 A owns (290, 20],  B owns (20, 110],  C owns (110, 200],  D owns (200, 290]

Note that the hash of a node and the hash of a key live in the same space, and nothing about a key's position depends on how many nodes exist. That independence is the whole trick.

3. Lookup: clockwise successor via binary search

Implementation is simple: keep the node positions in a sorted array. To find a key's owner, hash the key and binary-search for the first position greater than (or equal to) the key's hash. If you run off the end of the array, wrap around to index 0 (that's the "ring").

positions = [20, 110, 200, 290]          # sorted node tokens
owner     = {20: "A", 110: "B", 200: "C", 290: "D"}

lookup(hash = 75):   first position > 75   → 110 → "B"
lookup(hash = 290):  first position >= 290 → 290 → "D"   (bisect_left treats equal as owned)
lookup(hash = 300):  nothing > 300         → wrap to index 0 → 20 → "A"
key"user:42:timeline"
→hash
positione.g. 1,734,221,009
→binary search
successor tokenwrap at end
→map
nodecache-c

Cost: with M total positions on the ring, a lookup is O(log M) — for 100 servers × 200 virtual nodes = 20,000 positions, that's about 15 comparisons, i.e. well under a microsecond. Adding a node is O(M) with a sorted array (insertion) or O(log M) with a balanced tree — irrelevant in practice because membership changes are rare. Every client computes the same answer independently, as long as everyone has the same membership list (distributed via config service, gossip, or a ZooKeeper/etcd watch).

4. Adding and removing nodes: only ~K/N keys move

Suppose a new node E hashes to a position between B and C. Which keys change owner?

So adding a node only steals keys from one neighbour, and removing a node only hands its keys to one neighbour (its successor). With K keys and N nodes evenly spread, a change moves about K/N keys — the theoretical minimum, since the new node must end up with its fair share.

Before: 4 nodes After: node E added A B C D A B C D E k1→Bk5→Bk2→C k3→Dk4→A k1→Bk5→B k3→Dk4→A k2→E ✱ positions growclockwise ↻ only arc (B, E]changes owner Orange arc = keys that moved from C to E. All other keys keep their owner.
The ring before and after adding node E. Keys (dots) are coloured by owner. Only k2, which lies in the arc between B and E, changes owner (C → E). With mod-N hashing most keys would have moved.

4.1 Worked example: 1,000,000 keys

Changemod-N hashing: keys movedConsistent hashing (ideal): keys moved
4 → 5 nodes (add)800,000 (80%)200,000 (1/5 = 20%), all to the new node
4 → 3 nodes (one fails)750,000 (75%)250,000 (the failed node's 1/4), all to survivors
10 → 11 nodes~909,000 (91%)~91,000 (1/11 ≈ 9%)
100 → 101 nodes~990,000 (99%)~9,900 (1/101 ≈ 1%)

The Python demo in Section 7 measures this on 100,000 real keys: adding a 5th node to 4 moved 19.6% of keys (ideal 20%), and every moved key went to the new node; mod-N would have moved 80%.

⚠️ The catch with one position per node With a single token per node, the arcs between randomly placed nodes are very uneven, and when a node leaves, its entire load lands on one neighbour — which may then overload and fail too, cascading around the ring. Both problems are solved by virtual nodes.

5. Virtual nodes: balance and heterogeneous capacity

5.1 Why one point per node is unbalanced

N random points split a circle into arcs whose lengths vary a lot: the largest arc is typically several times the average (on the order of log N times). Measured on 200,000 keys and 10 nodes using the implementation below:

Ring positions per nodeStd-dev of load (as % of mean)Busiest node vs meanLeast-busy node vs mean
1122%3.63×0.07×
1034%1.81×0.52×
1008.0%1.13×0.88×
1,0002.2%1.04×0.97×

With one position each, one server got 3.6× its fair share while another got almost nothing. The spread shrinks roughly like 1/√V where V is the number of positions per node.

5.2 The fix: many virtual nodes per physical node

Give each physical node V virtual nodes (vnodes): hash "cache-a#0", "cache-a#1", …, "cache-a#99" to get 100 positions, all owned by cache-a. Lookup is unchanged — find the successor position, then map it to its physical node.

A1 A2 A3 A4 B1 B2 B3 B4 C1 C2 C3 C4 key K walk clockwise 3 physical nodes × 4 vnodes each (real systems use ~16–256 per node) • Each node owns many small, scattered arcs → load evens out (std-dev ≈ 1/√V) • If C fails, its 4 arcs pass to 4 different successors → load spreads over A and B • Bigger server? Give it more vnodes (weight 2 → twice the positions → ~2× keys) • Key K → first vnode clockwise = B1 (primary) • Replicas (N = 3): keep walking, skip nodes already chosen: B1, A2, (B2 skip), C2 → replica set [B, A, C]
Virtual nodes: each physical node (A, B, C) appears at several ring positions. Ownership arcs interleave, so load is balanced and a failure spreads its keys across many survivors. The dashed arrow shows the clockwise walk used to pick N = 3 distinct replicas for key K.

Benefits of virtual nodes:

Costs: a bigger ring (N × V positions — still tiny: 1,000 servers × 256 = 256,000 sorted integers), and in databases, more token ranges to repair and stream. Cassandra used num_tokens: 256 by default for years; Cassandra 4.0 lowered the default to 16 with a smarter token-allocation algorithm that places tokens to balance load deliberately rather than randomly.

6. Replication along the ring

To store each key on N replicas (see Replication), Dynamo-style systems use the ring itself: the key's primary is its successor node, and the other replicas are the next distinct physical nodes clockwise. The ordered list is called the key's preference list.

  1. Hash the key; find the first vnode clockwise → its physical node is replica #1.
  2. Keep walking clockwise. For each vnode, if its physical node is already in the list, skip it (otherwise two "replicas" could be the same machine — no redundancy).
  3. Stop when you have N distinct physical nodes. Optionally also skip nodes in the same rack or availability zone until each replica is in a different failure domain — Cassandra's NetworkTopologyStrategy does this per data center, placing replicas on distinct racks.

Nice properties: every node can compute any key's replica set locally; when a node joins or leaves, preference lists change only for keys near it on the ring; and during a temporary failure, the next node in the walk can accept writes on its behalf (the sloppy quorum and hinted handoff covered in Replication).

7. A complete Python implementation

The whole algorithm fits in ~60 lines: a sorted list of vnode positions, a dict from position to physical node, and bisect for the binary search. It supports weights, removal and replica sets.

import bisect
import hashlib
from collections import Counter


def _hash(value: str) -> int:
    """Map a string to a point on the ring: 0 .. 2**32 - 1."""
    digest = hashlib.md5(value.encode("utf-8")).digest()
    return int.from_bytes(digest[:4], "big")


class ConsistentHashRing:
    def __init__(self, vnodes_per_weight: int = 100):
        self.vnodes_per_weight = vnodes_per_weight
        self._points = []          # sorted list of vnode positions
        self._owner = {}           # position -> physical node name
        self._nodes = {}           # physical node -> list of its positions

    def add_node(self, node: str, weight: int = 1) -> None:
        positions = []
        for i in range(self.vnodes_per_weight * weight):
            pos = _hash(f"{node}#vn{i}")
            if pos in self._owner:          # extremely rare collision: skip it
                continue
            self._owner[pos] = node
            bisect.insort(self._points, pos)
            positions.append(pos)
        self._nodes[node] = positions

    def remove_node(self, node: str) -> None:
        for pos in self._nodes.pop(node):
            del self._owner[pos]
            idx = bisect.bisect_left(self._points, pos)
            self._points.pop(idx)

    def get_node(self, key: str) -> str:
        if not self._points:
            raise RuntimeError("ring is empty")
        h = _hash(key)
        idx = bisect.bisect_left(self._points, h)    # first vnode at or clockwise of h
        if idx == len(self._points):                 # past the last point: wrap to 0
            idx = 0
        return self._owner[self._points[idx]]

    def get_nodes(self, key: str, n: int) -> list:
        """Replica set: walk clockwise, collecting n DISTINCT physical nodes."""
        if n > len(self._nodes):
            raise ValueError("not enough physical nodes")
        result = []
        idx = bisect.bisect_left(self._points, _hash(key))
        while len(result) < n:
            if idx == len(self._points):
                idx = 0
            node = self._owner[self._points[idx]]
            if node not in result:                   # skip other vnodes of chosen nodes
                result.append(node)
            idx += 1
        return result


if __name__ == "__main__":
    ring = ConsistentHashRing(vnodes_per_weight=100)
    for name in ["cache-a", "cache-b", "cache-c", "cache-d"]:
        ring.add_node(name)

    keys = [f"user:{i}:timeline" for i in range(100_000)]
    before = {k: ring.get_node(k) for k in keys}
    print("Load with 4 nodes:", sorted(Counter(before.values()).items()))

    ring.add_node("cache-e")
    after = {k: ring.get_node(k) for k in keys}
    moved = sum(1 for k in keys if before[k] != after[k])
    print("Load with 5 nodes:", sorted(Counter(after.values()).items()))
    print(f"Keys moved: {moved} ({moved / len(keys):.1%}); ideal = 1/5 = 20.0%")
    print("All moved keys went to cache-e:",
          all(after[k] == "cache-e" for k in keys if before[k] != after[k]))

    naive_moved = sum(1 for i in range(100_000) if i % 4 != i % 5)
    print(f"Naive mod-N would move: {naive_moved} ({naive_moved / 100_000:.1%})")

    print("Replicas for user:42:timeline ->", ring.get_nodes("user:42:timeline", 3))

    ring.remove_node("cache-b")
    gone = {k: ring.get_node(k) for k in keys}
    moved_b = sum(1 for k in keys if after[k] != gone[k])
    print(f"Removing cache-b moved {moved_b} keys, all previously on cache-b:",
          all(after[k] == "cache-b" for k in keys if after[k] != gone[k]))

Actual output (Python 3, deterministic because MD5 is deterministic):

Load with 4 nodes: [('cache-a', 26028), ('cache-b', 22725), ('cache-c', 23324), ('cache-d', 27923)]
Load with 5 nodes: [('cache-a', 20640), ('cache-b', 19816), ('cache-c', 18519), ('cache-d', 21391), ('cache-e', 19634)]
Keys moved: 19634 (19.6%); ideal = 1/5 = 20.0%
All moved keys went to cache-e: True
Naive mod-N would move: 80000 (80.0%)
Replicas for user:42:timeline -> ['cache-b', 'cache-d', 'cache-e']
Removing cache-b moved 19816 keys, all previously on cache-b: True

What to notice:

💡 Production details Use a fast non-cryptographic hash (MurmurHash3, xxHash) rather than MD5 if you hash millions of keys per second — any well-mixed hash works, but every client must use exactly the same hash function, vnode naming and membership list, or they'll disagree about owners. Python's built-in hash() is randomised per process for strings, so it must never be used for this.

8. Where consistent hashing is used

SystemHow it uses consistent hashing
Apache Cassandra / ScyllaDBToken ring with Murmur3 partitioner (64-bit tokens); each node owns several token ranges (vnodes, num_tokens); replicas = next distinct nodes clockwise, rack/DC-aware. Adding a node streams only its new ranges.
Amazon Dynamo (2007 paper), Riak, VoldemortThe canonical design: ring + virtual nodes + preference lists + sloppy quorums. Riak uses a fixed number of equal-sized partitions on the ring.
Amazon DynamoDBDescends from Dynamo's ideas (hash-partitioned by partition key), though the managed service maps partitions via its own metadata rather than a client-visible ring.
Memcached clients — ketamalibketama (Last.fm, 2007) popularised client-side consistent hashing for memcached pools; it's implemented by most memcached client libraries and proxies (twemproxy, Nginx hash … consistent). Adding a cache server invalidates only ~1/N of keys.
Load balancersSession/cache affinity: Envoy's RING_HASH and MAGLEV policies, HAProxy hash-type consistent (with optional bounded loads), Nginx hash $key consistent, Google's Maglev. Requests for the same user/URL go to the same backend, and backend changes reshuffle few clients. See Load Balancing.
CDNs and web cachesInside a PoP, the URL is hashed to pick which cache server stores it, so the PoP's total cache is the union (not N copies) of server caches. The 1997 consistent-hashing paper was written for exactly this problem. See CDN.
Distributed hash tables, chat and messagingChord/Kademlia DHTs (peer-to-peer), routing chat rooms or user sessions to a stateful server (e.g. which WebSocket node handles a conversation — see Direct Messaging).

Contrast: Redis Cluster uses 16,384 fixed hash slots (CRC16(key) mod 16384) assigned to nodes — the "fixed number of logical partitions" approach from Sharding, which achieves the same goal (move slots, not keys) with an explicit slot map instead of a ring.

9. Alternatives and refinements

9.1 Rendezvous hashing (Highest Random Weight, HRW)

For each key, compute a score for every node — score = hash(node_id + key) — and pick the node with the highest score. For N replicas, take the top-N scores.

def hrw_owner(key: str, nodes: list) -> str:
    return max(nodes, key=lambda node: _hash(f"{node}:{key}"))

9.2 Jump consistent hash

Google's jump consistent hash (Lamping & Veach, 2014) maps a 64-bit key to a bucket number in [0, num_buckets) with a tiny loop, no memory, O(ln N) time and near-perfect balance:

def jump_hash(key: int, num_buckets: int) -> int:
    b, j = -1, 0
    while j < num_buckets:
        b = j
        key = (key * 2862933555777941757 + 1) & 0xFFFFFFFFFFFFFFFF   # 64-bit LCG step
        j = int((b + 1) * (float(1 << 31) / float((key >> 33) + 1)))
    return b

9.3 Consistent hashing with bounded loads

Even with vnodes, a ring balances keys, not load: a few very hot keys can overload their owner. Consistent hashing with bounded loads (Mirrokni, Thorup & Zadimoghaddam, Google, 2016) sets a capacity per node of c = ⌈(1 + ε) × average load⌉. A request goes to its usual successor unless that node is at capacity, in which case it continues clockwise to the next node with room.

 ε = 0.25, 4 servers, 100 active connections  →  average 25, cap = ⌈1.25 × 25⌉ = 32 each

 key K hashes near server B:   B has 32/32 (full) → skip
                               next clockwise: C has 20/32 → assign to C ✔
 Guarantee: no server exceeds 1.25× the average, while most keys still land on their
 "natural" server (good cache affinity) and changes still move few keys.

Vimeo contributed this to HAProxy (hash-balance-factor) to keep video-cache affinity without overloading any single cache server. It's ideal for load balancing with affinity, where load matters more than strict key placement.

9.4 Comparison

SchemeLookup costMemoryBalanceKeys moved on changeArbitrary node removalBest for
mod NO(1)NoneExcellent~all (N/(N+1))Yes, but reshuffles everythingFixed-size clusters that never change
Ring + vnodesO(log NV)N·V positionsGood (≈1/√V)~K/N✅ YesCaches, Dynamo-style databases
Rendezvous (HRW)O(N)Node listExcellent~K/N✅ YesSmall/medium node sets; easy replica lists
Jump hashO(log N)NoneExcellent~K/(N+1)❌ Only last bucketNumbered shards / replica groups
Bounded loadsO(log NV) + skipsRing + load countersGuaranteed ≤ (1+ε)×avgSmall, plus overflow moves✅ YesLoad balancers with affinity, hot keys
MaglevO(1) table lookupLookup table (e.g. 65,537 entries)Near-perfectSmall (slightly more than minimal)✅ YesHigh-throughput L4 load balancers
Fixed slots (Redis Cluster)O(1)Slot mapDepends on slot assignmentOnly moved slots✅ Yes (reassign slots)Databases/caches with a coordinator

10. How Instagram uses consistent hashing

📸 In Instagram's architecture

10.1 Example: scaling the feed cache before a big event

  1. Assume 40 Redis timeline servers, 100 vnodes each, and ~400 M cached timelines (~10 M per server). Traffic is forecast to rise 25%, so ops adds 10 servers.
  2. The new servers' vnodes are inserted into the ring; the new membership list is published via the config service, and every Feed Service instance rebuilds its ring (a few ms).
  3. Each new server takes ~1/50 of the ring → in total 10/50 = 20% of timelines (≈80 M keys) now map to new servers and miss once. With mod-N, 40/50 = 80% (≈320 M) would have missed.
  4. Misses are refilled lazily from Cassandra (or by pre-warming: copying the affected ranges before flipping the membership). Because those 20% are spread evenly over time and users, the database sees a manageable bump instead of a stampede. See Caching for stampede protection.

11. Trade-offs & common pitfalls

✅ Benefits

  • Minimal key movement (~K/N) when nodes join or leave
  • Fully decentralised: every client computes owners locally
  • With vnodes: good balance, graceful failure spreading, weighted capacity
  • Natural replica placement (next distinct nodes clockwise)
  • Simple: a sorted array and a binary search

⚠️ Limitations

  • Balances keys, not request load — hot keys still overload one node
  • Without vnodes, arcs are very uneven
  • All clients must agree on membership, hash and vnode scheme
  • Range queries are impossible across a hash ring
  • Moved keys are still a cache-miss burst (smaller, not zero)

Common pitfalls

12. Real-world technologies

TechnologyMechanismWhen to use it
Cassandra / ScyllaDBMurmur3 token ring, vnodes, rack/DC-aware replica placementDistributed database with automatic partitioning and multi-DC replication.
Riak, VoldemortDynamo-style ring with fixed partitionsHighly available key-value stores (largely legacy now).
libketama, twemproxy, mcrouterClient/proxy-side consistent hashing for memcached/Redis poolsSharding a cache tier without a central coordinator.
Envoy (RING_HASH, MAGLEV)Ring hash with configurable ring size; Maglev tableService-mesh load balancing with affinity (e.g. by user ID header).
HAProxy (hash-type consistent, hash-balance-factor)Consistent hashing, optionally with bounded loadsCache affinity at the load balancer without overload.
Nginx (hash $key consistent)Ketama-compatible ringUpstream selection for caches/proxies.
Guava Hashing.consistentHashJump consistent hashMapping keys to numbered shards in Java.
Redis Cluster16,384 fixed hash slots (not a ring)Sharded Redis with explicit slot migration.

13. Interview corner

Explain consistent hashing and why it's better than hash mod N.

With mod N, a key's server depends on N, so changing N remaps about N/(N+1) of keys — 80% when going from 4 to 5 servers — which empties a cache or forces a massive data migration. Consistent hashing hashes both servers and keys onto the same circular space; a key belongs to the first server clockwise. A key's position doesn't depend on how many servers exist, so adding a server only takes over the arc between it and its predecessor (~K/N keys), and removing one only hands its arc to its successor. Lookup is a binary search over sorted server positions, O(log M).

What are virtual nodes and why do we need them?

Each physical server is placed at many ring positions (e.g. 100–256) by hashing "server#i". With one position each, random arcs are very uneven (a node can get 3–4× its share) and a departing node dumps all its keys on one neighbour. With many vnodes, each server owns many small interleaved arcs, so load evens out (spread ≈ 1/√V), a failure is absorbed by all survivors, a joining node pulls from many peers in parallel, and weights are easy — give bigger machines more vnodes.

How do you choose replicas on a consistent hash ring?

Walk clockwise from the key's position, collecting the physical nodes of the vnodes you encounter, skipping any physical node (and ideally any rack/AZ) already chosen, until you have N distinct ones — the key's preference list. The first is the coordinator/primary. Every node can compute it locally, and membership changes affect only nearby keys. Cassandra's NetworkTopologyStrategy does this per data center with rack awareness.

Your cache cluster uses consistent hashing, but one server is at 95% CPU while others are at 30%. Why, and what do you do?

Either the key distribution is uneven (too few vnodes, or a poor hash) or — more likely at Instagram scale — a few hot keys (a celebrity's profile/timeline, a viral post's counter) live on that server; consistent hashing balances keys, not traffic. Check per-key request rates. Fixes: more vnodes or weight adjustments for distribution problems; for hot keys, replicate them to several servers and read randomly among copies, add an in-process L1 cache in app servers, split the key, or use consistent hashing with bounded loads at the routing layer.

Compare consistent hashing, rendezvous hashing and jump hash.

Ring hashing: O(log M) lookup, needs vnodes for balance, supports arbitrary node add/remove — the classic for caches and Dynamo-style stores. Rendezvous (HRW): score every node with hash(node, key) and take the max; perfectly balanced without vnodes, minimal disruption, easy top-N replicas, but O(N) per lookup. Jump hash: no memory, O(log N), excellent balance and minimal movement, but buckets are numbered 0..n−1 and only the last can be removed — great for numbered shards backed by replica groups, not for individually failing nodes.

Is consistent hashing used for Instagram's PostgreSQL shards?

Not per Instagram's public description. For PostgreSQL they use a fixed number of logical shards (thousands) mapped to physical servers by a mapping table, with the logical shard ID embedded in every object ID. That achieves the same goal — growing without re-hashing — via explicit shard moves. Consistent hashing shows up where membership changes are frequent and decentralised: cache tiers (memcache/Redis) and Cassandra's token ring.

14. Check your understanding

With hash mod N, what fraction of keys move when going from 8 to 9 servers? With an ideal consistent hash?

Mod N: a key stays only if h mod 8 = h mod 9, true for 8 of every 72 values → ~11% stay, ~89% move. Consistent hashing: the new server takes its fair share, 1/9 ≈ 11%, and nothing else moves.

Ring positions are [100, 400, 700] for nodes [X, Y, Z] on a ring of size 1,000. Who owns keys hashing to 50, 400, 401 and 950?

Using "first position ≥ hash": 50 → 100 → X; 400 → 400 → Y; 401 → 700 → Z; 950 → past the end → wrap to 100 → X.

In the same ring, node W is added at position 550. Which keys move, from whom, and what fraction (assuming uniform keys)?

Keys in (400, 550] move from Z to W; nothing else changes. That's 150/1,000 = 15% of keys. (With only one position per node, the share depends on where W lands — another reason for vnodes.)

A cluster has 10 nodes with 1 token each, and the busiest node has 3.6× the average load. Roughly how many vnodes per node would bring the spread to about ±8%?

Spread shrinks roughly like 1/√V. Our measurement showed ~122% std-dev at V = 1 and ~8% at V = 100 (busiest node ≈ 1.13× average). Real systems use 16–256 vnodes or smarter token allocation.

Why must you skip vnodes when selecting N = 3 replicas by walking the ring? Give a failure scenario if you don't.

Consecutive vnodes may belong to the same physical node. Without skipping, a key's "three replicas" could be cache-b, cache-b, cache-d — two copies on one machine. If cache-b dies, two of three replicas vanish and a quorum (2 of 3) becomes impossible, even though "only one node" failed.

You have 3 cache servers: two with 64 GB RAM and one with 128 GB. How do you give the big one its fair share with consistent hashing?

Weight vnodes by capacity: e.g. 100 vnodes for each 64 GB server and 200 for the 128 GB server. The big server then owns ~200/400 = 50% of the ring, the others ~25% each — proportional to RAM.

Why is jump consistent hash a poor fit for a memcached pool where any server may crash, but fine for 64 numbered Cassandra-like replica groups?

Jump hash can only shrink by removing the highest-numbered bucket. If server #17 crashes, you can't remove bucket 17 without renumbering (and remapping) everything above it. In a store where each bucket is a replicated group that never disappears (a failed machine is replaced inside the group), bucket numbers are stable and jump hash's zero-memory perfect balance is ideal.

🎯 Key takeaways

Related lessons