Library / SDK
buraksezer/consistent avatar
buraksezer/consistent

consistent gives a Go hash ring a bounded-load guarantee

Consistent hashing with bounded loads in Golang

785 stars81 forksGoMIT

At a glance

What is it?
buraksezer/consistent is a Go implementation of consistent hashing with bounded loads, the algorithm described in the Google Research and arXiv papers the README links. It distributes keys across partitions, assigns partitions to members so that no member exceeds the average load, and locates a key with a single hash and a modulo.
Who is it for?
Go teams that need to place keys on a changing set of nodes should use this library, because the bounded-load guarantee is the part that is hard to add yourself, and the implementation stays small, with a single dependency-free module and a configuration surface of five fields.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 55 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The bounded-load variant of consistent hashing

consistent provides a consistent hashing function which simultaneously achieves both uniformity and consistency, which is the formulation from the Google Research blog post the README links as the primary source, alongside the arXiv paper on consistent hashing with bounded loads and a post on improving load balancing from the Vimeo engineering blog.

That last set of references matters, because this is not the classic consistent hashing algorithm. Classic consistent hashing is popular because membership changes move only a small fraction of keys, and it accepts that some nodes end up noticeably more loaded than others. The bounded-load variant keeps the key-movement property and adds a guarantee: no member is assigned more than the average load.

For a cache or a key-value store, that difference decides whether the system degrades gracefully. With a plain ring, one hot member can absorb several times its share of the traffic and become the bottleneck, and the only fix is to change the ring or the hash function. With bounded loads, the distribution is capped by construction, and the algorithm decides how to trade a little extra movement for the balance.

The library is small by design. The module is github.com/buraksezer/consistent, it declares Go 1.22, and it has no dependencies at all, which is unusual for a distributed systems component and makes it easy to vendor into a service that already has a crowded dependency graph.

The audience is Go developers who control a cluster of members and need to map a key to one of them, which in practice means cache sharding, partition placement and routing tables.

How the ring is built and how a key is found

The overview section describes the model in the package's own terms. In this package's context, the keys are distributed among partitions and partitions are distributed among members as well. That two-level structure is the design decision worth understanding, because it separates two problems that a single ring would mix.

When you create a new consistent instance or call Add and Remove, three things happen. The member's name is hashed and inserted into the hash ring. The average load is calculated by the algorithm defined in the paper. And partitions are distributed among members by hashing partition IDs, with none of them exceeding the average load.

The last clause is the guarantee, and it has a hard failure mode that the README states plainly. Average load cannot be exceeded, so if all members are loaded at the maximum while trying to add a new member, it panics. That is a deliberate design choice rather than a bug: the library would rather fail loudly than silently produce a ring that violates its own invariant.

Locating a key is the cheap half. When you call LocateKey, the key, a byte slice, is hashed, the result of the hash is mod by the number of partitions, and that modulo result is the partition in which the key will be located. The owner of the partition is already determined before LocateKey is called, so it returns the partition owner immediately.

The last line of the overview is the one that decides whether this is usable in a hot path. No memory is allocated by consistent except hashing when you want to locate a key. For a proxy or a cache router on every request, that is the property that matters.

Five configuration fields, and one of them is permanent

The configuration is a single struct, and the comments in the source are more informative than the field names.

go
type Config struct {
	// Hasher is responsible for generating an unsigned, 64-bit hash of the provided byte slice.
	Hasher Hasher

	// Keys are distributed among partitions. Prime numbers are good to
	// distribute keys uniformly. Select a big PartitionCount if you have
	// too many keys.
	PartitionCount int

	// Members are replicated on consistent hash ring. This number means that a member
	// how many times replicated on the ring.
	ReplicationFactor int

	// Load is used to calculate the average load. See the code, the paper and Google's blog post to learn about it.
	Load float64

	// ReplicaKey is a function that builds the ring key of a virtual node.
	ReplicaKey func(name string, idx int) []byte
}

Hasher is any hash algorithm that implements the Hasher interface, and the README points at the Sample section for an example, so the choice of hash is yours rather than fixed.

PartitionCount is the number of slices the key space is divided into, and the comment says prime numbers are good to distribute keys uniformly and to select a big count if you have too many keys. ReplicationFactor is how many times a member is replicated on the ring, which is the classic virtual node mechanism. Load is the multiplier used to calculate the average load, and the comment sends you to the code, the paper and the Google post rather than explaining it here, which is a fair sign that the value is not a free parameter. ReplicaKey is a function that builds the ring key of a virtual node, so you can control how replicas are named on the ring.

And then there is the constraint that matters most for planning. The number of partitions cannot be changed after creation. A ring is therefore a permanent decision, which is why the comment on PartitionCount tells you to pick a big one.

The adoption list reads like a Kubernetes and blockchain index

The README keeps a Notable Users section, and it is worth reading for two reasons: it shows the library is not theoretical, and it shows the shape of the problems it is used for.

Several entries are Kubernetes operators. The Amazon CloudWatch Agent Operator manages the CloudWatch Agent on Kubernetes, the OpenTelemetry Operator manages the OpenTelemetry Collector, the Azure prometheus-collector is the Azure Monitor managed service for Prometheus, and the authorizer access-controller is a distributed access-control server based on Google Zanzibar. Those are all cases where a set of shard or replica owners has to be derived from a membership list that changes as nodes come and go.

Several are data and blockchain infrastructure. Olric is a distributed in-memory key-value store and cache, KeyDB is a distributed key store designed to be eventually consistent, motan-go is a cross-language RPC framework, Erda is an enterprise cloud-native application platform, Zadig is a continuous delivery product, and Chainlink and the Celo blockchain and Conflux's Infura equivalent and the Atlas chain are in the same list. Placing data or work on a changing set of nodes is the recurring problem.

The rest are infrastructure pieces: EdgeMesh simplifies network and services for edge applications, the Aibrix project provides pluggable components for GenAI inference, Easegress is a cloud native traffic orchestration system, and InfiniCache is a memory cache built on ephemeral serverless functions.

The pattern is consistent. Wherever a set of members changes and something must be routed to the right one, this ring is the mechanism. The list also tells you the library has been exercised in production by a range of projects, which is a stronger signal for a dependency of this kind than any documentation would be.

Where consistent is the wrong tool

The limits follow from the algorithm's own choices, and they are sharp.

First, the partition count is immutable. The README says it directly, and the design means a ring cannot be rebalanced by changing the number of slices. If your key volume grows past what the initial partition count can hold, you have to build a new ring and migrate, and the only lever left at creation time is the comment telling you to pick a large prime.

Second, the library panics under overload. If all members are at the maximum load while a new member is being added, the process panics rather than returning an error or relaxing the bound. In a long-running service that is a decision you have to accept deliberately, and it is the kind of failure that only shows up during an incident.

Third, this is a hash ring and nothing more. There is no membership gossip, no health checking, no client, no server and no transport. The library answers one question, which member owns this key, and the surrounding system has to agree on membership through some other means. For a cache, that is exactly the boundary you want, but for a system where membership itself is the hard problem, this is one component of the solution.

Fourth, the key space is uniform in the sense of the algorithm, not of your data. A partition count chosen for a billion keys is wasteful for ten thousand, and the distribution guarantee applies to keys, not to the bytes or the value sizes behind them. The load bound therefore protects the partition count, not your bandwidth.

Fifth, it is Go only, and the module has no dependencies, which is a virtue for vendoring and a limit for a polyglot platform.

Classic hashing versus bounded loads, and where the versions stand

The alternative approach to the same problem is the classic consistent hash ring, and the difference in approach is instructive.

A classic ring hashes members and keys onto a circle and assigns each key to the next member clockwise. It is simple, well understood, and its defining property is that adding or removing a member moves only the keys that belonged to the affected range. What it does not promise is balance. Depending on the hash and the number of members, some members get noticeably more than their share, and with a small member count the variance is large.

The bounded-load variant this library implements keeps the movement property and adds a cap. The cost of the cap is that the algorithm has to make harder choices when the ring changes, and those choices are documented in the paper rather than in the code, which is why the README's comment on the Load field points you at the paper, the code and the Google post.

If your membership is static, the classic ring is enough and this library adds a failure mode you do not need. If membership changes and you have seen one member become hot, the bounded-load guarantee is worth the extra complexity, and a library that already implements it is worth more than a weekend of work.

On versions, the history is short. The repository published v0.10.0 on 2022-11-09, then v1.0.0 on 2026-08-16 and v1.1.0 on 2026-08-29, with the last push on 2026-08-29. The project is not archived, and the licence is MIT with a LICENSE file at the root, so there is nothing unusual about depending on it. A 1.0 release after a long stretch of 0.x is also a reasonable signal that the API has stopped moving, which matters more for a library like this than the release count does.

Editorial conclusion

Go teams that need to place keys on a changing set of nodes should use this library, because the bounded-load guarantee is the part that is hard to add yourself, and the implementation stays small, with a single dependency-free module and a configuration surface of five fields. It is the wrong choice if your membership set never changes, since plain hashing is simpler, and it is the wrong choice if you need a partition count that can move later, because the number of partitions cannot be changed after creation. Before adopting, pick a large prime partition count appropriate to your key volume, decide the hasher and the replication factor deliberately, and read the paper so you understand what the load bound costs when members are added or removed.

Frequently asked questions

What does the consistent library guarantee about load?

When you create an instance or call Add and Remove, the average load is calculated by the algorithm from the paper, and partitions are distributed among members so that none exceeds that average load. The average load cannot be exceeded, so adding a member when all members are already at the maximum causes a panic.

How do I install the consistent Go library?

Run go get github.com/buraksezer/consistent with a correctly configured Go environment. The module declares Go 1.22 and has no dependencies, and the README points to the _examples folder for usage samples.

Can I change the number of partitions after creating a consistent ring?

No. The README states that the number of partitions cannot be changed after creation, which is why the Config comment advises choosing a big PartitionCount, and a prime number, if you expect a lot of keys.

Does LocateKey allocate memory on every call?

No memory is allocated by consistent except hashing when you want to locate a key. LocateKey hashes the key, takes the result mod the partition count, and returns the partition owner, which was already determined when the ring was built.

Official sources

  1. buraksezer/consistent on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/buraksezer-consistent.svg)](https://hysenlabs.com/projects/buraksezer-consistent)

Was this useful?