Chapter 30: Consistent Hashing & Data Placement

Introduction

Consistent hashing is the foundational technique for horizontal data partitioning in distributed systems. It solves a fundamental problem: how to distribute data across N servers such that adding or removing servers doesn't force a complete reshuffle.

This chapter covers:

  • Why naive hashing (hash(key) % N) fails catastrophically when N changes
  • Consistent hashing's hash ring concept and virtual nodes
  • Modern variants: bounded-load, jump hash, rendezvous, Maglev
  • Real implementations in DynamoDB, Cassandra, Memcached, Nginx
  • Performance trade-offs and implementation details
Interview Signal

Strong candidates explain why consistent hashing minimizes data movement (only K/N keys move) and can sketch the ring with virtual nodes. Weak candidates memorize "hash ring" without understanding redistribution mechanics or the virtual node problem.

1. The Problem: Naive Hashing

1.1 Naive Approach: Modulo Hashing

The obvious approach to distribute keys across N servers:

Naive Hash
server = hash(key) % N

This works perfectly until N changes. When you add or remove a server, nearly every key remaps to a different server.

1.2 Catastrophic Redistribution

Key Hash N=3 (Before) N=4 (After) Moved?
user:123 0x7A3F 0x7A3F % 3 = 0 0x7A3F % 4 = 3 YES
user:456 0x5C12 0x5C12 % 3 = 2 0x5C12 % 4 = 2 NO
user:789 0x9A81 0x9A81 % 3 = 1 0x9A81 % 4 = 1 NO
user:012 0x2E4B 0x2E4B % 3 = 1 0x2E4B % 4 = 3 YES
user:345 0xB7D9 0xB7D9 % 3 = 2 0xB7D9 % 4 = 1 YES

Result: Adding 1 server (N=3 → N=4) remapped 3 out of 5 keys (60%). In reality, with millions of keys, ~75% of all data must move.

Naive Hashing: Before and After Adding Server
graph LR subgraph "Before: N=3" K1[Key A
hash%3=0] --> S0A[Server 0] K2[Key B
hash%3=1] --> S1A[Server 1] K3[Key C
hash%3=2] --> S2A[Server 2] K4[Key D
hash%3=0] --> S0A K5[Key E
hash%3=1] --> S1A end subgraph "After: N=4" K1B[Key A
hash%4=3] -.moved.-> S3B[Server 3] K2B[Key B
hash%4=1] --> S1B[Server 1] K3B[Key C
hash%4=2] --> S2B[Server 2] K4B[Key D
hash%4=0] -.moved.-> S0B[Server 0] K5B[Key E
hash%4=1] --> S1B end

1.3 Why This Matters

  • Cache systems: All cache entries invalidate → thundering herd on backend
  • Data stores: Massive data movement, network saturation, hours of downtime
  • Elastic scaling: Can't add capacity without destroying performance
Core Problem

Modulo hashing creates a global dependency between the hash value and the number of servers. Changing N invalidates the entire mapping.

2. Consistent Hashing Concept

2.1 The Hash Ring

Consistent hashing (Karger et al., 1997) maps both keys and nodes to the same hash space, forming a conceptual ring.

Hash Space
Ring: [0, 2^32) or [0, 2^128) depending on hash function
Both keys and nodes hash to positions on this ring

Key idea: Each key is assigned to the first node encountered when traveling clockwise from the key's position on the ring.

Consistent Hash Ring (3 Nodes, 6 Keys)
graph TB subgraph "Hash Ring [0, 2^32)" R[" "] end N0["Node A
hash(A)=100M"] N1["Node B
hash(B)=1.5B"] N2["Node C
hash(C)=3B"] K0["Key X → Node A"] K1["Key Y → Node B"] K2["Key Z → Node C"] style N0 fill:#34d399 style N1 fill:#60a5fa style N2 fill:#a78bfa

2.2 Key Mapping Algorithm

function get_node(key, nodes):
    key_hash = hash(key)

    # Find first node clockwise from key_hash
    for node in sorted(nodes, key=lambda n: hash(n)):
        if hash(node) >= key_hash:
            return node

    # Wrap around: return first node
    return sorted(nodes, key=lambda n: hash(n))[0]

2.3 Why This Works

Property Naive Hash Consistent Hash
Add node ~K × (N-1)/N keys move ~K/N keys move
Remove node ~K × (N-1)/N keys move ~K/N keys move
Load distribution Uniform if N doesn't change Uneven with few nodes
Lookup cost O(1) O(log N) binary search
Interview Tip

When asked "how many keys move when you add a server?", walk through the math: with K keys and N nodes, adding 1 node means ~K/N keys in the new node's clockwise arc must move. Contrast with naive hashing where K×(N/(N+1)) keys stay in place but the rest move.

3. Virtual Nodes

3.1 Why Virtual Nodes

Basic consistent hashing has a fatal flaw: with few physical nodes, the ring positions are uneven, causing severe load imbalance.

Scenario Node A Range Node B Range Node C Range Imbalance
Physical nodes only 45% of ring 30% of ring 25% of ring 1.8× difference
V=10 virtual nodes 34% of ring 33% of ring 33% of ring 1.03× difference
V=100 virtual nodes 33.4% of ring 33.2% of ring 33.4% of ring 1.006× difference

Solution: Each physical node is represented by V virtual nodes at different positions on the ring.

Virtual Node Hashing
virtual_node_position[i] = hash(physical_node_id + ":" + i)
for i in [0, V)
Virtual Nodes on Hash Ring (V=3 per physical node)
graph LR subgraph "Physical Nodes" PA[Node A] PB[Node B] end subgraph "Virtual Nodes on Ring" VA1["A:0
pos=50M"] VA2["A:1
pos=1.2B"] VA3["A:2
pos=2.8B"] VB1["B:0
pos=300M"] VB2["B:1
pos=1.9B"] VB3["B:2
pos=3.5B"] end PA -.owns.-> VA1 PA -.owns.-> VA2 PA -.owns.-> VA3 PB -.owns.-> VB1 PB -.owns.-> VB2 PB -.owns.-> VB3 style PA fill:#34d399 style PB fill:#60a5fa

3.2 Tuning V

V Value Load Balance Memory Cost Lookup Cost Use Case
1 Poor Minimal O(log N) Not recommended
10-50 Acceptable Low O(log N×V) Small clusters (N < 10)
100-200 Good Moderate O(log N×V) Medium clusters (10-100 nodes)
500+ Excellent High O(log N×V) Large clusters (rare, use alternatives)

Cassandra default: V=256 (historically), V=16 in modern versions

DynamoDB: Uses virtual nodes internally, not exposed to users

Trade-off

Higher V improves load balance but increases memory (must store V×N positions) and lookup time (binary search over V×N entries). Practical sweet spot: V=100-200 for most systems.

4. Adding/Removing Nodes

4.1 Redistribution Mechanics

When a new node joins, it claims responsibility for a fraction of the key space. Only keys in its clockwise arc must move.

Adding Node D Between A and B
graph TD subgraph "Before: 3 Nodes" K1[Keys 0-100M] --> A1[Node A] K2[Keys 100M-200M] --> B1[Node B] K3[Keys 200M-300M] --> C1[Node C] end subgraph "After: 4 Nodes (D added at 50M)" K4[Keys 0-50M] --> A2[Node A] K5[Keys 50M-100M] -.moved.-> D[Node D] K6[Keys 100M-200M] --> B2[Node B] K7[Keys 200M-300M] --> C2[Node C] end style D fill:#fbbf24

4.2 Mathematical Proof

Keys Redistributed When Adding Node
Expected keys moved = K / (N + 1)

Proof:
- New node D claims 1/(N+1) of the ring on average
- Keys uniformly distributed → K/(N+1) keys in that arc
- Those keys previously belonged to D's clockwise successor
- Only those keys move; all others stay put
Operation Keys Moved Example (K=1M, N=10)
Add node (naive) K × N/(N+1) ≈ 909K 90.9% of data moves
Add node (consistent) K / (N+1) ≈ 91K 9.1% of data moves
Remove node (naive) K × (N-1)/N ≈ 900K 90% of data moves
Remove node (consistent) K / N = 100K 10% of data moves

4.3 Minimal Disruption Guarantee

Consistent hashing provides a monotonicity property:

  • Adding a node only causes keys to move to the new node
  • Removing a node only causes its keys to move to its successor
  • Keys never shuffle between existing nodes
Interview Question

Q: "Why is consistent hashing better than mod-N?"
A: "With mod-N, changing N invalidates almost all mappings because the modulo creates a global dependency. Consistent hashing decouples nodes from each other—each node owns an arc of the ring, and changes only affect neighboring arcs. This reduces data movement from O(K) to O(K/N)."

5. Bounded-Load Consistent Hashing

Standard consistent hashing has no protection against hot spots—if keys skew to one arc, a single node becomes overloaded.

5.1 The Problem

Scenario Node A Load Node B Load Node C Load Issue
Uniform key distribution 33% 33% 34% Balanced
Popular key "user:celebrity" 70% 15% 15% A overloaded

5.2 Bounded-Load Solution (Mirrokni et al., Google 2018)

Set a load cap for each node:

Bounded-Load Formula
max_load[node] = (1 + ε) × (total_load / N)

Typical ε = 0.25 (25% over average)

Algorithm:

  1. Start with standard consistent hash lookup
  2. If target node is at max capacity, skip to next clockwise node
  3. Repeat until finding a node under capacity
  4. Track load in a separate data structure (not the ring)
function get_node_bounded(key, nodes, load_map, epsilon):
    key_hash = hash(key)
    avg_load = total_keys / len(nodes)
    max_load = (1 + epsilon) * avg_load

    candidates = sorted(nodes, key=lambda n: hash(n) if hash(n) >= key_hash
                                              else hash(n) + 2^32)

    for node in candidates:
        if load_map[node] < max_load:
            return node

    # Fallback: return least loaded
    return min(nodes, key=lambda n: load_map[n])

5.3 Performance Impact

ε Value Max Overload Lookup Cost Balance Quality
0 (no bound) Unlimited O(log N) Poor under skew
0.25 +25% O(log N) avg Good
0.1 +10% O(N) worst case Excellent
Industry Usage

Google: Uses bounded-load consistent hashing for Cloud Storage and Bigtable load balancing. Cloudflare: Adopted for edge server load distribution.

6. Jump Consistent Hash

Jump Consistent Hash (Google, 2014) is a radically simpler algorithm that eliminates the hash ring entirely.

6.1 Algorithm Deep Dive

// C++ reference implementation (Lamping & Veach, Google 2014)
int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) {
    int64_t b = -1, j = 0;
    while (j < num_buckets) {
        b = j;
        key = key * 2862933555777941757ULL + 1;
        j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1));
    }
    return b;
}

How it works:

  1. Starts with bucket -1, then "jumps" forward based on pseudorandom values derived from the key
  2. Each iteration computes: "if we had j+1 buckets, would this key still be in bucket b?"
  3. Loop terminates when j reaches num_buckets
  4. Returns the last valid bucket index

6.2 Properties

Property Jump Hash Consistent Hash Ring
Memory O(1) - no data structure O(N×V) - store ring positions
Lookup time O(log N) expected jumps O(log N×V) binary search
Balance Perfect (no virtual nodes needed) Depends on V
Add bucket Exactly K/(N+1) keys move ~K/(N+1) keys move
Remove arbitrary bucket NOT SUPPORTED Supported

6.3 Limitations

Critical Constraint

Jump hash requires sequential bucket IDs [0, N-1]. You cannot remove an arbitrary bucket—only the last one. This makes it unsuitable for systems where nodes have persistent identities (like server hostnames) or where any node can fail.

6.4 Use Cases

  • Good: Client-side sharding where you control bucket count (e.g., 16 shards)
  • Good: Static partitioning (e.g., partition log files across 100 buckets)
  • Bad: Distributed caches where servers come and go
  • Bad: Systems needing heterogeneous node weights
Interview Tip

If asked about jump hash, immediately clarify: "It's O(1) memory and perfectly balanced, but it only works for sequential buckets—you can't remove arbitrary nodes. It's great for static partitioning but not for elastic clusters."

7. Rendezvous Hashing (Highest Random Weight)

Rendezvous hashing (Thaler & Ravishankar, 1998) takes a completely different approach: for each key, compute a score for every node and pick the highest.

7.1 Algorithm

function rendezvous_hash(key, nodes):
    max_score = -1
    selected_node = None

    for node in nodes:
        score = hash(key + node.id)  # Combine key and node
        if score > max_score:
            max_score = score
            selected_node = node

    return selected_node

Key insight: Since the score depends on both key and node, adding/removing a node only affects keys where that node had the highest score.

7.2 Comparison

Aspect Rendezvous Consistent Hash Ring
Lookup time O(N) - check all nodes O(log N×V)
Memory O(N) - just node list O(N×V) - ring positions
Balance Perfect (no virtual nodes) Depends on V
Add/remove any node Supported Supported
Weighted nodes Easy: duplicate in list Adjust V per node

7.3 Optimization: Top-K Candidates

For replication, rendezvous naturally extends to "pick top K nodes by score":

function rendezvous_top_k(key, nodes, k):
    scores = [(node, hash(key + node.id)) for node in nodes]
    scores.sort(key=lambda x: x[1], reverse=True)
    return [node for node, score in scores[:k]]

7.4 Use Cases

  • Small N: When you have 10-100 nodes, O(N) lookup is acceptable
  • Weighted nodes: Easy to give powerful nodes more replicas
  • Simple implementation: No complex ring data structure
Real Usage

Tahoe-LAFS: Uses rendezvous for replica placement. GlusterFS: Originally used consistent hashing, later added rendezvous option for better balance.

8. Maglev Hashing

Maglev (Google, 2016) is designed for high-performance load balancers. It pre-computes a lookup table for O(1) lookups with minimal disruption.

8.1 Concept

Instead of a hash ring, Maglev builds a fixed-size table where each entry points to a backend. Keys hash to table indices.

Maglev Lookup
backend = lookup_table[hash(key) % M]

M = table size (typically 65537, a prime)

8.2 Table Population Algorithm

function populate_maglev_table(backends, M):
    table = [None] * M

    # Generate permutation for each backend
    permutations = {}
    for backend in backends:
        offset = hash1(backend) % M
        skip = hash2(backend) % (M - 1) + 1
        perm = [(offset + i * skip) % M for i in range(M)]
        permutations[backend] = perm

    # Fill table round-robin
    indices = {b: 0 for b in backends}
    filled = 0

    while filled < M:
        for backend in backends:
            idx = indices[backend]
            while idx < M:
                pos = permutations[backend][idx]
                indices[backend] += 1
                idx += 1

                if table[pos] is None:
                    table[pos] = backend
                    filled += 1
                    break

    return table

Properties:

  • Each backend gets ~M/N entries in the table
  • Backends are evenly distributed (no virtual nodes needed)
  • When a backend is added/removed, only ~1/N entries change

8.3 Performance

Operation Time Complexity Notes
Lookup O(1) Just hash + array access
Build table O(M × N) Done rarely (on config change)
Memory O(M) M = 65537 → ~64KB for IDs

8.4 Connection Disruption

Algorithm Connections Disrupted (Add 1/N nodes)
Naive hash ~100% × N/(N+1)
Consistent hash (V=100) ~1/(N+1) worst case
Maglev ~1/(N+1) guaranteed
Google Maglev LB

Google's Maglev load balancer serves millions of requests per second with sub-millisecond latency. The lookup table fits in CPU cache (L2/L3), making lookups extremely fast. Used for Google Cloud Load Balancing and YouTube traffic.

9. Real Systems

9.1 DynamoDB

Architecture:

  • Partition key → MD5 hash → 128-bit value → maps to partition
  • Consistent hashing with virtual nodes (internal, not exposed)
  • Each partition ~10GB, splits automatically when exceeded
// DynamoDB partition assignment (conceptual)
partition_key = "user:12345"
hash_value = MD5(partition_key)  // 128-bit
partition = ring.find_node(hash_value)

// Replication: N=3 replicas across AZs
replicas = [partition, partition.successor, partition.successor.successor]
DynamoDB Partition Key to Node Mapping
graph LR PK[Partition Key
'user:12345'] --> H[MD5 Hash
0x7A3F...8B2C] H --> R[Hash Ring
Lookup] R --> N1[Node A
Primary] R --> N2[Node B
Replica 1] R --> N3[Node C
Replica 2] style N1 fill:#34d399 style N2 fill:#60a5fa style N3 fill:#a78bfa

9.2 Cassandra

Architecture:

  • Token ring: each node claims a range of tokens (virtual nodes)
  • Default: V=256 (older) or V=16 (3.0+, tunable)
  • Partition key → Murmur3 hash → token → node
  • Replication factor (RF) determines how many nodes store each key
Setting Value Impact
num_tokens 16 (default) Number of virtual nodes per physical node
partitioner Murmur3Partitioner Hash function (128-bit output)
replication_factor 3 (typical) How many nodes store each partition
// Cassandra token ring (simplified)
CREATE KEYSPACE my_keyspace WITH replication = {
  'class': 'NetworkTopologyStrategy',
  'datacenter1': 3
};

// Key lookup
key = "user:12345"
token = murmur3(key) % 2^63  // Signed 64-bit range
coordinator = token_ring.find_node(token)
replicas = coordinator.successors(RF=3)

9.3 Memcached

Client-side consistent hashing: Memcached server has no knowledge of the ring—clients implement it.

// libmemcached consistent hashing (C)
memcached_st *memc = memcached_create(NULL);
memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_DISTRIBUTION,
                       MEMCACHED_DISTRIBUTION_CONSISTENT_KETAMA);

// Ketama algorithm (default): MD5 hash with 160 virtual nodes per server
// Client computes: hash(key) → binary search on ring → server

Ketama algorithm:

  • Each server gets 160 virtual nodes (40 hashes × 4 positions each)
  • MD5 hash: hash(server_address + ":" + i) for i in [0, 40)
  • Take 4 × 32-bit slices from each 128-bit MD5 output

9.4 Nginx

Upstream consistent hash directive:

upstream backend {
    hash $request_uri consistent;
    server backend1.example.com;
    server backend2.example.com;
    server backend3.example.com;
}

# Hash the request URI, use consistent hashing to pick server
# Minimizes cache misses when backend servers change

Virtual nodes: Nginx uses 160 virtual nodes per server by default (same as Ketama).

System Comparison: Hash Ring Usage
graph TB subgraph "DynamoDB" D1[Partition Key] --> D2[MD5] --> D3[Internal Ring
Virtual Nodes] end subgraph "Cassandra" C1[Partition Key] --> C2[Murmur3] --> C3[Token Ring
V=16 default] end subgraph "Memcached" M1[Key] --> M2[Client: MD5] --> M3[Ketama Ring
V=160] end subgraph "Nginx" N1[Request URI] --> N2[Hash] --> N3[Upstream Ring
V=160] end

10. Algorithm Comparison

Algorithm Memory Lookup Add Node Remove Any Node Balance Best Use Case
Naive Hash O(1) O(1) O(K) keys move O(K) keys move Perfect Fixed cluster size
Consistent Hash O(N×V) O(log N×V) O(K/N) keys O(K/N) keys Depends on V Elastic clusters, caches
Bounded-Load CH O(N×V) O(N) worst O(K/N) keys O(K/N) keys Excellent Skewed workloads
Jump Hash O(1) O(log N) O(K/N) keys Only last bucket Perfect Static partitioning
Rendezvous O(N) O(N) O(K/N) keys O(K/N) keys Perfect Small N, weighted nodes
Maglev O(M) O(1) O(K/N) keys O(K/N) keys Excellent Load balancers

Decision Matrix

Requirement Recommended Algorithm
Need to remove arbitrary nodes Consistent Hash, Rendezvous, Maglev
Minimize memory (millions of keys) Jump Hash, Rendezvous
Minimize lookup latency Maglev, Naive Hash
Handle hot keys Bounded-Load Consistent Hash
Static partition count (no elasticity) Jump Hash
Simple implementation Rendezvous (if N small)
Heterogeneous node weights Rendezvous, Consistent Hash

11. Implementation Considerations

11.1 Hash Function Choice

Hash Function Output Size Speed Collision Resistance Use Case
MD5 128 bits Moderate Broken (crypto) DynamoDB, Memcached (legacy)
MurmurHash3 32/128 bits Fast Good (non-crypto) Cassandra, Redis
xxHash 32/64/128 bits Very Fast Excellent Modern systems
SHA-1 160 bits Slow Broken (crypto) Avoid
CRC32 32 bits Very Fast Poor Checksums only

Recommendation: Use xxHash for new implementations (fast, good distribution). Use MurmurHash3 for compatibility with Cassandra/Redis ecosystems.

11.2 Replication with Consistent Hashing

Store data on the primary node and its N-1 clockwise successors:

function get_replica_nodes(key, ring, replication_factor):
    primary = ring.find_node(hash(key))
    replicas = [primary]

    current = primary
    while len(replicas) < replication_factor:
        current = ring.successor(current)
        # Skip if same physical node (virtual nodes)
        if current.physical_id not in [r.physical_id for r in replicas]:
            replicas.append(current)

    return replicas

Rack/Zone Awareness:

  • Cassandra: NetworkTopologyStrategy ensures replicas in different racks/DCs
  • DynamoDB: Replicas always in different Availability Zones

11.3 Preference Lists

DynamoDB-style preference list: pre-compute N replicas for fast quorum operations.

// Preference list: ordered list of nodes for a key
class PreferenceList {
    primary: Node
    replicas: Node[]  // Length = N

    function quorum_write(key, value, W):
        responses = []
        for node in replicas:
            responses.append(node.write_async(key, value))

        # Wait for W successes
        return wait_for_n(responses, W)

    function quorum_read(key, R):
        responses = []
        for node in replicas:
            responses.append(node.read_async(key))

        # Wait for R responses, resolve conflicts
        results = wait_for_n(responses, R)
        return resolve_conflicts(results)
}

11.4 Handling Node Failures

Scenario Solution
Node temporarily down Hinted handoff: buffer writes at successor, replay when node returns
Node permanently removed Remove from ring, keys redistribute to successor
Node replaced (new hardware) Assign same token/position on ring, restore data from replicas
Network partition Sloppy quorum: accept writes at any N healthy nodes, not just preference list

11.5 Code Example: Consistent Hash Ring

class ConsistentHashRing:
    def __init__(self, nodes, virtual_nodes=100):
        self.virtual_nodes = virtual_nodes
        self.ring = {}  # position -> node_id
        self.sorted_positions = []

        for node in nodes:
            self.add_node(node)

    def add_node(self, node):
        for i in range(self.virtual_nodes):
            vnode_key = f"{node}:{i}"
            position = xxhash.xxh64(vnode_key).intdigest()
            self.ring[position] = node

        self.sorted_positions = sorted(self.ring.keys())

    def remove_node(self, node):
        for i in range(self.virtual_nodes):
            vnode_key = f"{node}:{i}"
            position = xxhash.xxh64(vnode_key).intdigest()
            del self.ring[position]

        self.sorted_positions = sorted(self.ring.keys())

    def get_node(self, key):
        if not self.ring:
            return None

        position = xxhash.xxh64(key).intdigest()

        # Binary search for first node clockwise
        idx = bisect.bisect_right(self.sorted_positions, position)
        if idx == len(self.sorted_positions):
            idx = 0  # Wrap around

        return self.ring[self.sorted_positions[idx]]

    def get_nodes(self, key, n):
        """Get n distinct physical nodes for replication"""
        if not self.ring:
            return []

        position = xxhash.xxh64(key).intdigest()
        idx = bisect.bisect_right(self.sorted_positions, position)

        nodes = []
        seen = set()
        count = 0

        while len(nodes) < n and count < len(self.sorted_positions):
            if idx >= len(self.sorted_positions):
                idx = 0

            node = self.ring[self.sorted_positions[idx]]
            if node not in seen:
                nodes.append(node)
                seen.add(node)

            idx += 1
            count += 1

        return nodes
Interview Coding Question

Q: "Implement a consistent hash ring with add/remove node and get_node methods."
Key points:

  • Use a sorted list of positions for binary search (O(log N×V) lookup)
  • Virtual nodes: hash node_id + ":" + vnode_index
  • Handle wrap-around (if search passes end of ring, return first node)
  • For replication, track physical nodes to avoid duplicates

12. Interview Signals

Strong Signals

  • Explains why naive hashing fails: "Modulo creates a global dependency on N—changing N remaps almost all keys"
  • Quantifies data movement: "Consistent hashing moves only K/N keys vs K×(N-1)/N for naive"
  • Mentions virtual nodes unprompted: "With few nodes, you need virtual nodes to balance the ring"
  • Knows alternative algorithms: "Jump hash is O(1) memory but can't remove arbitrary nodes"
  • Discusses real systems: "Cassandra uses Murmur3 with 16 vnodes by default, down from 256"
  • Addresses hot keys: "Standard consistent hashing doesn't prevent hot spots—you need bounded-load or sharding"

Weak Signals

  • Only memorizes "hash ring" without understanding redistribution math
  • Can't explain why virtual nodes are needed
  • Doesn't know any real system implementations
  • Confuses consistent hashing with quorum consensus
  • Can't compare consistent hash vs jump hash vs rendezvous

Common Mistakes

Pitfall 1: "Consistent hashing guarantees perfect balance"

Wrong: Without virtual nodes, balance can be very poor (2x-3x difference between nodes).
Right: "You need V=100-200 virtual nodes per physical node to get good balance."

Pitfall 2: "Consistent hashing solves hot key problems"

Wrong: If one key is extremely popular, its node will be overloaded.
Right: "Consistent hashing distributes keys evenly across the ring, but doesn't handle skewed access patterns. You need bounded-load hashing or application-level sharding for hot keys."

Pitfall 3: "Jump hash is always better than consistent hash"

Wrong: Jump hash can't remove arbitrary nodes.
Right: "Jump hash is great for static partitioning where you control the bucket count, but it doesn't work for elastic clusters where any node can fail."

Sample Interview Questions

Q1: Design a distributed cache (Memcached-like) with 100 servers

Answer:

  1. Client-side consistent hashing: Clients maintain a hash ring with all 100 servers
  2. Virtual nodes: Each server gets V=100 virtual nodes for balance
  3. Hash function: xxHash or MurmurHash3 (fast, good distribution)
  4. Key lookup: hash(key) → binary search on ring → server
  5. Add server: Update ring, only ~1/101 of keys need to move (no need to rehash everything)
  6. Remove server: Remove from ring, keys redistribute to clockwise successor
  7. Health checking: Clients periodically ping servers, remove dead ones from ring
Q2: Why does DynamoDB use consistent hashing instead of range partitioning?

Answer:

  • Range partitioning risk: Sequential keys create hot partitions (e.g., timestamps, auto-incrementing IDs)
  • Hash partitioning advantage: Randomizes key distribution, prevents hot spots
  • Elastic scaling: Adding partitions doesn't require global coordination (only K/N keys move)
  • Trade-off: Can't do range scans across partitions—need to use Global Secondary Indexes
Q3: You add a new server to a consistent hash ring with 10 existing servers. How many keys move, and where do they come from?

Answer:

  1. Expected keys moved: ~K/11 (where K = total keys)
  2. Source: Keys come from the new server's clockwise successor on the ring
  3. Reason: The new server claims an arc of the ring that previously belonged to one other server
  4. With virtual nodes: Keys come from multiple servers (each vnode steals from its successor), but still ~K/11 total
  5. Contrast with naive hash: mod-N would remap ~K×10/11 ≈ 909K keys (90.9%)
Q4: When would you use Jump Hash instead of Consistent Hash?

Use Jump Hash when:

  • You control the bucket count (e.g., fixed sharding: 16, 32, 64 buckets)
  • Buckets are sequential IDs [0, N-1]
  • You only add buckets, never remove arbitrary ones
  • You want O(1) memory (no ring data structure)

Use Consistent Hash when:

  • Nodes have persistent identities (server hostnames, not just IDs)
  • Any node can fail or be removed
  • You need heterogeneous node weights
  • You need rack/zone-aware replica placement
Q5: How does Cassandra handle replication with consistent hashing?

Answer:

  1. Partition key → token: token = Murmur3(partition_key)
  2. Primary node: First node clockwise from token on ring
  3. Replicas: Next RF-1 distinct physical nodes clockwise
  4. NetworkTopologyStrategy: Ensures replicas in different racks/DCs
  5. Virtual nodes (num_tokens=16): Each physical node occupies 16 positions on ring
  6. Coordinator: Any node can coordinate read/write, forwards to replicas
  7. Quorum: W=2, R=2 with RF=3 → overlap guarantees consistency