A small, colorful Python project for exploring how keys find servers on a hash ring.
I wanted to understand what really happens when servers are added or removed from a distributed system.
The usual hashing idea is simple:
hash(key) % number_of_servers
That works until the number of servers changes. Add one new server and the modulo value changes. Suddenly, many keys may point somewhere else.
Consistent hashing takes a calmer path:
Put servers on a ring.
Put keys on the same ring.
Move clockwise from the key until a server is found.
That one idea makes the system much less jumpy when servers join or leave.
flowchart LR
K1["user-1"] --> H1["hash(user-1)"]
H1 --> R["Hash Ring"]
R --> C{"First server clockwise?"}
C --> S1["server-A"]
K2["order-9"] --> H2["hash(order-9)"]
H2 --> R
C --> S2["server-B"]
K3["payment-7"] --> H3["hash(payment-7)"]
H3 --> R
C --> S3["server-C"]
classDef key fill:#FEF3C7,stroke:#F59E0B,color:#111827,stroke-width:2px;
classDef hash fill:#DBEAFE,stroke:#2563EB,color:#111827,stroke-width:2px;
classDef ring fill:#F3E8FF,stroke:#7C3AED,color:#111827,stroke-width:3px;
classDef server fill:#D1FAE5,stroke:#059669,color:#111827,stroke-width:2px;
class K1,K2,K3 key;
class H1,H2,H3 hash;
class R,C ring;
class S1,S2,S3 server;
The project has one main class:
ConsistentHashRing
It supports:
add_node(node)
remove_node(node)
get_node(key)
get_nodes(key, count)
The ring keeps three important structures:
| Structure | Purpose |
|---|---|
ring |
Maps hash position to real server |
sorted_hashes |
Keeps all ring positions sorted for clockwise lookup |
nodes |
Stores the real servers currently available |
One real server appears multiple times on the ring.
With replicas = 3, server-A becomes:
server-A:0
server-A:1
server-A:2
Each label gets a different hash position, but all of them point back to server-A.
This helps spread keys more evenly.
flowchart TB
A["server-A"] --> A0["server-A:0"]
A --> A1["server-A:1"]
A --> A2["server-A:2"]
B["server-B"] --> B0["server-B:0"]
B --> B1["server-B:1"]
B --> B2["server-B:2"]
A0 --> R["Sorted Hash Ring"]
A1 --> R
A2 --> R
B0 --> R
B1 --> R
B2 --> R
classDef real fill:#FFE4E6,stroke:#E11D48,color:#111827,stroke-width:2px;
classDef virtual fill:#E0F2FE,stroke:#0284C7,color:#111827,stroke-width:2px;
classDef ring fill:#DCFCE7,stroke:#16A34A,color:#111827,stroke-width:3px;
class A,B real;
class A0,A1,A2,B0,B1,B2 virtual;
class R ring;
flowchart TD
A["Input key"] --> B["Hash the key"]
B --> C["Find first ring position >= key hash"]
C --> D{"Position found?"}
D -- "Yes" --> E["Return mapped server"]
D -- "No" --> F["Wrap to first ring position"]
F --> E
classDef start fill:#FEF3C7,stroke:#F59E0B,color:#111827,stroke-width:2px;
classDef action fill:#DBEAFE,stroke:#2563EB,color:#111827,stroke-width:2px;
classDef decision fill:#F3E8FF,stroke:#7C3AED,color:#111827,stroke-width:2px;
classDef result fill:#D1FAE5,stroke:#059669,color:#111827,stroke-width:2px;
class A start;
class B,C,F action;
class D decision;
class E result;
get_nodes(key, count) walks clockwise and collects distinct real servers.
Example idea:
get_nodes("user-1", 3)
Could return:
["server-A", "server-D", "server-C"]
That gives one primary server and additional backup servers.
consistent_hashing_python/
src/
hash_ring.py
__init__.py
tests/
test_hash_ring.py
docs/
flow_diagram.md
simulation.py
README.md
python simulation.pyThe simulation shows:
Initial key mapping
Movement after removing a server
Movement after adding a server
Replica selection
python -m unittest discover -s testsCurrent result:
Ran 10 tests
OK
The neat part is that the system does not panic when the server list changes.
When a node disappears, only the keys owned by that node need a new home. When a node appears, it takes over just part of the ring. The rest of the keys stay exactly where they were.
That is the quiet power of consistent hashing.