DSA DSA In Depth
0%
lesson consistent-hashingrendezvous-hashingpartitioning

Consistent Hashing with Bounded Loads & Rendezvous (HRW) Hashing

7 min read Module 2.1: Hash Tables, Modern Probing & Probabilistic Sketches Updated Oct 23, 2026
📄 Cheat Sheet
Consistent Hashing | Virtual Nodes & Hash Ring Partitioning
ByteByteGo Consistent Hashing | Virtual Nodes & Hash Ring Partitioning Watch on YouTube ↗

Robert Morris and Frans Kaashoek explain how Karger's consistent hashing ring bounds key migration to O(K/N) during node churn and why virtual nodes are essential for load balance.

0:00 / --:--

Ring-Based Consistent Hashing vs Rendezvous (HRW) Hashing

Three algorithms dominate production traffic routing and shard assignment:

  • Karger's Ring with Virtual Nodes (VNodes): Map both keys and server replicas onto the 64-bit cyclic integer ring $[0, 2^{64}-1]$. Each physical server $s_i$ places $V$ virtual tokens $h(s_i \mathbin{\|} 0), \dots, h(s_i \mathbin{\|} V-1)$ onto a sorted array or balanced BST (std::map). A key $k$ routes to the first token clockwise with $\text{token} \ge h(k)$ via std::lower_bound in $O(\log(N V))$ time (wrapping around to index $0$ if $h(k)$ exceeds all tokens). Using $V = 150$ VNodes reduces load standard deviation to $\sigma / \mu \approx 1/\sqrt{150} \approx 8\%$.
  • Consistent Hashing with Bounded Loads (Mirrokni et al., 2017): Given total active load $W$, number of nodes $N$, and slack parameter $\epsilon \in (0, 1]$ (typically $\epsilon = 0.25$), each server is assigned a hard maximum capacity $C = \lceil (1 + \epsilon) W / N \rceil$. When a key hashes to its primary clockwise server, if that server's current load is $< C$, it accepts the key; otherwise the key walks clockwise to the next server with spare capacity.
  • Rendezvous / Highest Random Weight (HRW) Hashing: Requires zero ring state or VNode tables. To route key $k$ across nodes $\{s_1, \dots, s_N\}$, compute a joint score $w_i = \text{hash}(k, s_i)$ for each node and select $s^* = \arg\max_i w_i$. Achieving perfect $1/N$ uniform distribution without VNodes, HRW costs $O(N)$ per lookup (or $O(\log N)$ using a hierarchical skeleton tree), making it ideal for cluster sizes $N \le 64$ or weighted multi-replica selection (picking top-$R$ replicas in one pass).
Sharding Strategy Lookup Time Routing Table Memory Keys Moved on Node Churn Max Load Ratio (Hot Workload)
Modulo Hashing ($h(k) \bmod N$) O(1) O(1) $\frac{N-1}{N} \approx 100\%$ (Catastrophic) Unbounded on skew
Ring Consistent Hashing ($V$ VNodes) O(log(NV)) O(N · V) $\frac{1}{N}$ (Optimal) $1 + O\left(\sqrt{\frac{\log N}{V}}\right)$ (Uniform keys)
Bounded-Load Ring ($1 + \epsilon$) O(log(NV) + 1/ε²) O(N · V) $\le \frac{1 + O(\epsilon)}{N}$ Strict Cap $\lceil(1 + \epsilon) W/N\rceil$
Rendezvous / HRW Hashing O(N) O(N) (Zero VNodes) $\frac{1}{N}$ (Optimal, 0 variance bias) Uniform $1/N$ across nodes

Formal Specification: Karger Monotonicity & Bounded-Load Probe Theorem

Minimal Disruption Invariant: Let $S$ be the set of active nodes and $S' = S \cup \{s_{\text{new}}\}$. Under both Virtual-Node Ring Hashing and HRW Hashing, a key $k$ changes assignment ($f_{S'}(k) \ne f_S(k)$) if and only if $f_{S'}(k) = s_{\text{new}}$. No key ever migrates between two existing nodes $s_i, s_j \in S$. By symmetry under a uniform hash function, $\Pr[f_{S'}(k) = s_{\text{new}}] = \frac{1}{|S| + 1}$.

Bounded-Load Forwarding Bound (Mirrokni et al., 2017): For any arrival sequence of $W$ balls (even with arbitrary duplicate/hot keys), if each of the $N$ bins has capacity $C = \lceil (1 + \epsilon) W / N \rceil$, the expected number of clockwise forwarding hops to locate a non-full bin is bounded by $O(1 / \epsilon^2)$, independent of $N$ and $W$.

Visualizing the $2^{64}$ Ring, VNode Arcs & $(1+\epsilon)$ Bounded-Load Overflow

h(k) N1 N2 N3 N0 1. Bounded-Load Overflow Routing (ε = 0.25): • Key k hashes to arc (N0, N1] → Primary target = N1 • N1 is at cap C = ⌈1.25 · W/N⌉ (FULL) → Forward clockwise • N2 has load < C → Key k assigned to N2! 2. Rendezvous (HRW) Weight Comparison: w(k, N0)=0.41, w(k, N1)=0.92, w(k, N2)=0.87, w(k, N3)=0.19 Sorted ranking [N1, N2, N0, N3] gives primary + failover list without storing any VNode ring in memory!
flowchart LR K["Incoming Key k"] --> H["pos = hash64(k)"] H --> LB["idx = lower_bound(ring, pos) if idx == ring.size(): idx = 0"] LB --> Check{"node_load[ring[idx].node] < ceil((1 + ε) · W / N) ?"} Check -- "Yes (Capacity Available)" --> Assign["Increment node_load Return ring[idx].node"] Check -- "No (Node at Bounded Cap)" --> Step["idx = (idx + 1) % ring.size()"] --> Check
Consistent Hashing Ring with Virtual Nodes & Bounded Loads
O(log(N·V)) Lookup · 1/N Churn

Map nodes and keys onto the [0, 2^32 − 1] unit circle via binary search (upper_bound). Observe how adding or removing a node migrates only O(K/N) keys and how bounded load caps node capacity at ⌈c · K/N⌉.

Step 1 / 10
Custom Input:
Timeline

Invariant & State

Ring sorted array of V·N points; lookup via std::upper_bound in O(log(VN))
🧭 Live Hash Ring & Bucket Probe Telemetry — Consistent Hashing Ring with Virtual Nodes & Bounded Loads Probe Collision/Swap Hit/Placed
30° 75° 110° 165° 210° 250° 315° 350° B[0] 30° (NodeA#0) B[1] 75° (key_41→A#1) B[2] 110° (NodeA#1) B[3] 165° (NodeB#0) B[4] 210° (key_88→C#0) B[5] 250° (NodeC#0) B[6] 315° (NodeB#1) B[7] 350° (key_19→A#0)
[0]30°NodeA#0
[1]75°key_41→A#1
[2]110°NodeA#1
[3]165°NodeB#0
[4]210°key_88→C#0
[5]250°NodeC#0
[6]315°NodeB#1
[7]350°key_19→A#0
Ring Space
[0, 2^32 - 1]
Lookup Complexity
O(log(N·V))
Key Churn on Join
~1 / N keys
Step
1 / 4
[Step 1] Initialized 3 physical nodes × 2 vnodes sorted on ring [0°, 360°).

Hardware Reality: Contiguous Sorted Array vs std::map for VNode Rings

With $N = 1{,}000$ backend servers and $V = 160$ virtual nodes per server, the ring contains $160{,}000$ tokens. Storing the ring in a pointer-based std::map (Red-Black Tree) incurs $\approx 18$ pointer-chasing hops across 7.6 MB of heap memory, costing ~1,200 ns per packet routed. Storing the $160{,}000$ (token_u64, node_id_u32) pairs in a flat contiguous sorted std::vector (1.9 MB, fitting entirely inside L3 cache) allows std::lower_bound (or Eytzinger binary search) to route packets in ~45 ns—a $25\times$ speedup in L4/L7 load balancers like Envoy and Katran.

Production C++20 Implementation: Bounded-Load Consistent Ring & Rendezvous (HRW) Router

// C++20: Consistent Hashing with (1 + eps) Bounded Loads & Rendezvous (HRW) Hashing
#include <vector>
#include <cstdint>
#include <algorithm>
#include <cmath>

class BoundedConsistentHashRing {
    struct VNode {
        uint64_t token;
        uint32_t nodeId;
        bool operator<(const VNode& o) const noexcept { return token < o.token; }
    };

    std::vector<VNode> ring_;
    std::vector<uint32_t> loads_;
    uint32_t totalLoad_ = 0;
    double epsilon_;

    static uint64_t mix64(uint64_t x) noexcept {
        x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL;
        x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL;
        return x ^ (x >> 31);
    }

public:
    BoundedConsistentHashRing(uint32_t numNodes, uint32_t vnodesPerNode = 128, double eps = 0.25)
        : loads_(numNodes, 0), epsilon_(eps) {
        ring_.reserve(numNodes * vnodesPerNode);
        for (uint32_t n = 0; n < numNodes; ++n) {
            for (uint32_t v = 0; v < vnodesPerNode; ++v) {
                uint64_t seed = (static_cast<uint64_t>(n) << 32) | v;
                ring_.push_back({mix64(seed), n});
            }
        }
        std::sort(ring_.begin(), ring_.end());
    }

    uint32_t acquireRoute(uint64_t key) {
        const uint32_t N = static_cast<uint32_t>(loads_.size());
        const uint32_t cap = static_cast<uint32_t>(
            std::ceil((1.0 + epsilon_) * static_cast<double>(totalLoad_ + 1) / N)
        );

        uint64_t h = mix64(key);
        auto it = std::lower_bound(ring_.begin(), ring_.end(), VNode{h, 0});
        size_t idx = (it == ring_.end()) ? 0 : static_cast<size_t>(it - ring_.begin());

        for (size_t probes = 0; probes < ring_.size(); ++probes) {
            uint32_t candidate = ring_[idx].nodeId;
            if (loads_[candidate] < cap) {
                ++loads_[candidate];
                ++totalLoad_;
                return candidate;
            }
            idx = (idx + 1 == ring_.size()) ? 0 : idx + 1;
        }
        return ring_[idx].nodeId;
    }

    // Stateless Rendezvous (HRW) Hashing: O(N) time, O(1) auxiliary space
    static uint32_t rendezvousSelect(uint64_t key, const std::vector<uint32_t>& activeNodes) noexcept {
        uint32_t bestNode = 0;
        uint64_t maxWeight = 0;
        for (uint32_t nodeId : activeNodes) {
            uint64_t w = mix64(key ^ mix64(nodeId));
            if (w >= maxWeight) {
                maxWeight = w;
                bestNode = nodeId;
            }
        }
        return bestNode;
    }
};

Failure Modes & Adversarial Pitfalls

  • Correlated Key Hash and VNode Hash Functions: If you use the exact same unseeded hash function to map keys onto the ring AND to shard keys inside the local hash table on the target server, every key assigned to virtual node $v$ falls into a narrow sub-interval of the 64-bit hash space—causing severe clustering inside the server's local hash table! Always use independent seeds for ring routing vs local bucket indexing.
  • Forgetting Wrap-Around when lower_bound Returns ring.end(): Any key whose hash exceeds the largest token on the ring belongs to ring[0], closing the circle $[0, 2^{64}-1]$.
Check yourself Question 1 of 4
Concept Check Key Disruption Fraction in Consistent Hashing vs Modulo Sharding recall

When a cluster of N cache servers scales from N to N + 1 nodes, approximately what fraction of keys must be remapped to a different server under naive `hash(k) mod N` sharding versus Karger's Consistent Hashing?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why do Virtual Nodes (VNodes) alone fail to prevent server overload when a single 'celebrity' key receives 30% of all cluster requests, and how does Consistent Hashing with Bounded Loads (Mirrokni et al., 2017) handle capacity enforcement?

Key Takeaways

Modular sharding hash(k) mod N remaps (N-1)/N of all keys when N changes by 1; Consistent Hashing and Rendezvous Hashing move only an optimal 1/N fraction of keys.
Placing V = Θ(log N) virtual nodes per physical server on the 2^64 ring reduces partition size variance to σ/μ ≈ 1/√V and enables heterogeneous hardware weighting.
Consistent Hashing with Bounded Loads caps every node at ⌈(1 + ε)W/N⌉ active requests and spills overflow keys clockwise in O(1/ε^2) expected steps.
Rendezvous (HRW) Hashing computes argmax_i hash(key, node_i) to achieve zero-VNode uniform load distribution and natural top-k replica ordering in O(N) time.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue