Two consistent hashing concerns pull in opposite directions: server weights must scale each server's share of the hash space, while a baseline number of replicas per unit of weight is needed to keep the error margin low. Ketama handles the first, but it multiplies the second, and once requests can only be served by a subset of servers, the number of hash structures explodes.
The algorithm comes down to a ratio. For any two servers, $m S_1m$ and $mS_2m$, if $mS_1m$ should serve $mw\timesm$ the requests that $mS_2m$ serves, then $mH_1 = w\times H_2m$ hashes are associated with $mS_1m$. That weight lets a server's hash count follow its capacity instead of being fixed; Cloudflare weights by disk space, which is what the Pingora team has done, and compute-intensive workloads elsewhere weight by CPU or GPU count. Crucially, the weighting does not replace the constant scale factor described above — the baseline is still what bounds the error margin, and it is the lowest-weight servers that expose it. The library takes its name from the implementation it originated in.
What the baseline cannot absorb is the case where any server is not interchangeable with any other. Compliance requirements and enabled caching features mean any request is eligible only for some subset of the fleet, and this cannot be expressed by adding more hashes to one ring. Each distinct combination needs its own ring. Combinations multiply, so a handful of features becomes $m2^\text{handful} = \text{dozens}m$ separate consistent hash rings.
That multiplicity, not the ring construction itself, is where PBR's memory went: the excessive usage Ivan found in pingora-ketama, 6GB in some cases, came from storing the sheer number of hashes required to represent all of those rings and the features they encode.
The compact index trick and the hash-count reduction are worth separating: the first is a pure memory-packing win, the second changes the ring's behavior. Both landed in the same migration.
Packing the hash and the index
PBR's struct for storing hashes in the ring originally spent eight bytes per entry: four for the hash itself, and four for an index into a separate array of servers.
struct Point {
hash: u32,
index: u32,
}
Zaidoon pointed out that a 32-bit index is larger than the job requires. The ring is unlikely to ever coordinate more than roughly $2^{16} \approx 65\text{k}$ servers simultaneously, so a 16-bit index suffices.
struct PointV2 {
hash: u32,
index: u16,
}
Shrinking the field alone changes nothing in memory. Rust requires a struct's size to be a multiple of its largest (most aligned) field, so with a four-byte hash the minimum size stays at eight bytes. #[repr(packed)] is one workaround, but it carries well-known hazards. A less readable but safer alternative is to hold the hash and index in a raw byte array and expose them through getters; both approaches compile to identical code.
struct Point([u8; 6]);
impl Point {
fn hash(&self) -> u32 {
u32::from_ne_bytes(self.0[0..4].try_into().unwrap())
}
fn index(&self) -> u16 {
u16::from_ne_bytes(self.0[4..6].try_into().unwrap())
}
}
That change cuts the memory consumed by consistent hashing by 25%.
How many hashes are actually needed
Deriving the standard deviation for the general case of $k$ hashes per server — rather than the single-hash case — yields an expected value of $\text{Exp}_k = \frac{1}{N}$ and a standard deviation of $\text{SD}_k=\sqrt{\frac{(k+1)}{N(kN+1)}-\frac{1}{N^2}}$. The coefficient of variation, which measures accuracy, therefore follows $\text{CV}_k=\frac{\text{SD}_k}{\text{Exp}_k}=\sqrt{\frac{N-1}{(N*k+1)}}$.
Plotted, that curve exposes the flaw in simply adding hashes.

Each successive reduction in error margin costs roughly an order of magnitude more hashes per server. With a weighting factor $m_w$ of 625 on a base of 160 hashes, a server gets $k = 160\times625 = 100{,}000$ hashes — and the final 90,000 of them buy only a 0.7% error reduction.
Worse, the math assumes a continuous ring while the implementation uses 32-bit hashes that can collide, and collision probability climbs quickly as hash count grows. Collisions silently drop contributions to a server's share of volume and requests, producing unpredictable error. Simulated results with 32-bit hashes track the predicted error rate up to a point, then diverge: for data centers with 2048 servers, error rises between 10,000 and 100,000 hashes per server.

The upside is that the hash count per server can be cut by 90% with no appreciable error, which is a direct RAM reclaim.
Changing the ring without flooding origins
A smaller ring places some cacheable requests on different backends. Switching network-wide in one step would invalidate nearly all cached content and convert a memory optimization into a surge in origin traffic. PBR instead held both the old ketama ring and the new one in memory, letting the existing migration framework choose between them per request. The choice was stable per request hash and reversible: if anything looked wrong, new requests could be routed back through the old ring without a redeploy.
The rollout proceeded in layers, from small validation locations through progressively larger data center groups and finally to the rest of the world. Two dimensions were controlled separately — how much traffic used the new ring, and where that traffic was permitted to move. A global percentage rollout would have spread cache churn everywhere at once; scoping by data center kept the blast radius contained and made safety judgments possible.
Throughout, the team monitored backend-selection traces, ring-version counters, PBR connection errors, process memory, startup time, cache behavior and origin traffic. At 100% migration, the temporary old-ring path was removed.

The sharp drop in the memory comparison is the day the PBR version carrying the now-unused large rings was decommissioned. The net effect of the changes: 100TB less memory in use.

Availability
The changes ship in the pingora-ketama crate behind a currently unadvertised cargo feature. The v2 ring carries the compacted storage format, a faster sorting method and a scalable base hash count per node. Because stability and control drove the design, the v1 ring is unchanged from what pingora ketama has always used, and the library runs both at once, deciding per request which to apply.



