Consistent Hashing for Data Partitioning

Distributed Systems intermediate Building with AI 14 min read

Overview

Consistent hashing is the routing function that keeps distributed caches, databases, and pub/sub systems evenly balanced while nodes join and leave. Instead of mapping keys to shards with modulo arithmetic—which causes full reshuffles on membership changes—consistent hashing arranges nodes on a logical ring so only a narrow slice of keys move.

The approach underpins distributed key-value stores, vector search shards, and most CDN edge caches. Virtual nodes provide fine-grained control over load distribution, helping ops teams rebalance hot partitions without copying petabytes.

This lesson walks through the structure, failure handling, and implementation patterns you need to apply consistent hashing to new services, including how to layer replication factors and locality hints on top of the basic ring.

Core Concepts

Key Concept

A membership change only affects the key ranges between the departing node’s previous position and its successor, typically < 1/ring_size of total data.

Key Concept

Virtual nodes allow weighting: assign more positions to beefier machines so the ring automatically hands them more traffic.

How It Works

Building the Ring

Each node chooses v tokens by hashing node_id || replica_index. Tokens are stored in a sorted array. To route a key, hash it, binary search for the first token greater than the hash, and fallback to index 0 if you wrap around.

Replication and Preference Lists

To replicate, continue walking clockwise until you collect rf distinct physical nodes. Keep track of rack/region metadata and skip nodes that would violate anti-affinity rules. This yields a stable preference list used by Raft groups in an experimental distributed database.

Key Concept

Rings can be weighted per keyspace: maintain separate token sets for write-heavy and read-heavy workloads to prevent one skewed dataset from overwhelming the rest.

Practical Applications

Implementation Patterns

Code Examples

Go: Building the Token Array

type Token struct {
    Value uint32
    Node  string
}

func buildTokens(nodes []Node, virtuals int) []Token {
    tokens := make([]Token, 0, len(nodes)*virtuals)
    for _, n := range nodes {
        for v := 0; v < virtuals; v++ {
            h := hash32([]byte(fmt.Sprintf("%s-%d", n.ID, v)))
            tokens = append(tokens, Token{Value: h, Node: n.ID})
        }
    }
    sort.Slice(tokens, func(i, j int) bool { return tokens[i].Value < tokens[j].Value })
    return tokens
}

TypeScript: Lookup with Replication Factor

function locate(tokens: Token[], key: string, rf = 3): string[] {
  const hash = murmur(key)
  const owners = new Set()
  let idx = binarySearch(tokens, hash)
  while (owners.size < rf) {
    owners.add(tokens[idx].node)
    idx = (idx + 1) % tokens.length
  }
  return Array.from(owners)
}

Common Pitfalls

Key Concepts Recap

Further Reading