Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

17 Commits

Repository files navigation

Consistent Hashing Visualizer

An interactive teaching tool that shows how consistent hashing places keys on a hash ring, how virtual nodes balance load, and how that compares to naive hash(key) % N assignment.

Table of Contents

Overview

This project is a small full-stack visualizer for consistent hashing, the technique used by many distributed caches, databases, and load balancers to map keys to servers with minimal remapping when the cluster changes.

The Express API owns the ring: hashing, virtual nodes, assignment, migration diffs, statistics, and a naive-hashing comparison. The React client draws the ring (or naive buckets), animates key movement, and lets you preview topology changes before applying them.

State lives in memory on the server. Restarting the API clears nodes and keys.

What is Consistent Hashing?

Imagine the hash space as a circle (the hash ring), from 0 up to a large maximum, then wrapping around.

  • Nodes (servers) are hashed onto the circle.
  • Keys (data items) are hashed onto the same circle.
  • A key belongs to the first node encountered when walking clockwise from the key’s position. If nothing is found before the wrap, ownership goes to the first node on the ring.

When you add a node, it takes over only the arc that previously belonged to its clockwise successor. Other keys stay put.

When you remove a node, its keys move to the next clockwise node. The rest of the ring is unchanged.

Virtual nodes (implemented here) place several positions per physical server (name#0, name#1, …). That splits each server’s share of the circle into smaller arcs, which usually improves load balance.

flowchart LR
K["hash(key)"] --> P["Position on ring"]
P --> W["Walk clockwise"]
W --> V["First virtual node"]
V --> N["Owning physical node"]
Loading

Why Consistent Hashing?

Traditional (naive) placement is:

node = hash(key) % N

N is the number of servers. Changing N (add or remove a machine) changes the modulus, so most keys can move. That is expensive: cache misses, data copies, and hot spots during rebalance.

Consistent hashing maps both keys and nodes into a shared circular space. Only keys in the affected arcs move—typically a much smaller fraction, especially with virtual nodes.

This visualizer lets you switch among Consistent Hashing, Naive Hashing, and Side-by-Side so you can see that difference on the same node and key set.

Features

Verified in the current codebase:

  • Interactive SVG hash ring with physical nodes, virtual nodes, and keys
  • Add / remove named physical nodes
  • Add named keys and generate batches of random keys (up to 5,000 per request)
  • Key-to-node mapping list with tooltips
  • Virtual nodes per physical node, adjustable from 1–100 (default 3)
  • Clockwise lookup tracer (hash → position → walk → virtual node → physical node)
  • Migration measurement and animation when topology changes
  • Dry-run preview before applying add/remove node or virtual-node-count changes (consistent-hashing mode)
  • Statistics: counts, load variance, balance score, largest/smallest owner, migration percent
  • Distribution histogram of keys per node
  • Comparison modes: consistent, naive (hash % N), side-by-side
  • Naive bucket view with numbered slots
  • Event log, reduced-motion support (OS preference + optional override), skip-to-controls link
  • In-memory reset of the entire ring

How It Works

  1. You add physical nodes. Each node is hashed; V virtual nodes are created (name#index) and placed on the ring.
  2. You add keys. Each key is hashed to a 32-bit position.
  3. The server sorts virtual nodes by hash and assigns every key to the successor virtual node (clockwise), then to that vnode’s physical owner.
  4. In consistent mode, add/remove node and vnode-count changes open a preview (before/after stats and migration). Confirming applies the mutation.
  5. In naive mode, physical nodes are sorted by name and each key goes to hash(key) % N. Virtual-node count does not apply.
  6. Side-by-side keeps one shared topology and shows both assignment algorithms, including how many keys each algorithm moved.
  7. Lookup tracer requests step-by-step clockwise walk data and can play, pause, step, or replay the walk on the ring.

Architecture / Project Structure

consistent-hashing-visualizer/
├── client/ React + Vite UI
│ ├── index.html
│ ├── vite.config.js
│ ├── src/
│ │ ├── App.jsx Controls, API calls, layout
│ │ ├── main.jsx
│ │ ├── index.css
│ │ ├── components/ Ring, buckets, stats, tracer, preview, …
│ │ ├── hooks/ Reduced-motion preference
│ │ └── utils/ Ownership arcs, motion, colors, tests
│ └── public/
└── server/ Express API
├── index.js HTTP server, CORS, JSON
├── routes/ring.js REST endpoints and in-memory state
├── core/
│ ├── hash.js MD5 → 32-bit hash
│ ├── ring.js Ring, vnodes, lookup, preview, generate
│ ├── comparison.js Naive hashing + response shape
│ ├── migration.js Assignment diffs
│ ├── stats.js Load statistics
│ └── validators.js Ring invariants (used by tests)
└── test/ Node.js test runner suites

There is no persistence layer. Ring state is held in memory on the Express process.

Algorithm

Hash (Node crypto, MD5, first 8 hex digits parsed as a 32-bit integer):

hash(s) = parseInt(md5(s).hex[0..7], 16)

Virtual nodes for physical name N and count V:

vnode i = { id: "N#i", hash: hash("N#i") } for i in 0 .. V-1

Successor (clockwise owner):

sorted = virtual nodes sorted by hash ascending
findOwner(keyHash):
if sorted is empty: return null
return first vnode with hash >= keyHash
or sorted[0] if none (wrap-around)

Naive comparison (physical nodes sorted by name):

index = keyHash % nodeCount
owner = sortedPhysicalNodes[index]

Lookup steps on the server match findOwner and are rejected if they disagree with the stored assignment.

Technical Implementation

ConcernImplementation
HashingMD5 truncated to 32 bits (server/core/hash.js)
RingArray of virtual nodes, sorted by hash, linear successor scan (Array.find)
Nodes{ id, name, hash, virtualNodes[] }
Keys{ key, hash, assignedNode, assignedVirtualNode }
Virtual nodesname#index; count 1–100, default 3
StateModule-level arrays on the Express router (not a database)
MigrationDiff on assignedVirtualNode.id; new keys are not counted as topology migrations
PreviewDeep clone (structuredClone), mutate clone, redistribute, return before/after
VisualizationSVG ring; angle = (hash / 0xffffffff) * 2π; Framer Motion + rAF walks
Client stateReact useState / useRef; Axios to VITE_API_URL or http://localhost:5000/api/ring
TestsNode.js built-in node --test on server core and client utils

Successor search is linear, not a binary search or tree. Do not treat lookups as O(log V) unless that data structure is added later.

Tech Stack

TechnologyPurpose
React 19UI
Vite 7Client bundler and dev server
Tailwind CSS 4Styling
AxiosHTTP client
Framer MotionRing / key motion
Express 5REST API
corsCross-origin requests from the Vite app
dotenvOptional PORT loading
Node.js cryptoMD5 hashing and random key names
Node.js test runnerUnit / golden tests

Installation

Requires Node.js and npm. The client and server are separate packages (no root package.json).

git clone https://github.com/devs-diaries/Consistent_Hashing_Visualizer.git
cd Consistent_Hashing_Visualizer

API (default port 5000):

cd server
npm install
npm run dev

Use npm start for node index.js without nodemon.

UI (Vite; typically http://localhost:5173):

cd client
npm install
npm run dev

Run the two processes in separate terminals. Point the client at the API with VITE_API_URL if the API is not on localhost:5000.

Environment Variables

There is no .env.example. Variables that appear in code:

VariableWherePurpose
PORTServerHTTP port (default 5000)
VITE_API_URLClient (build/dev)Ring API base URL (default http://localhost:5000/api/ring)

Neither is required for local development with the defaults. Do not commit secrets; this app does not use API keys.

Usage

  1. Start the server, then the client, and open the Vite URL.
  2. Choose Consistent Hashing, Naive Hashing, or Side-by-Side.
  3. Enter a node name and click Add Node. In consistent mode, review the preview, then apply.
  4. Add keys by name, or generate a batch (+10 / +50 / +100 / +500 shortcuts).
  5. Watch placement on the ring (or naive buckets) and in Node-Key Mapping.
  6. Adjust Virtual Nodes per Physical Node (consistent / side-by-side) and compare load in the stats panel and histogram.
  7. Add or remove a node and observe migration percent, flying keys (unless reduced motion), and the migration summary.
  8. In consistent mode, type a key into the lookup tracer and Trace the clockwise walk (play / step / replay).
  9. Reset clears nodes, keys, vnode count, and mode back to consistent hashing.

Example

Conceptual ring (clockwise from a key):

 Node A (vnodes on the circle)
|
Key 1 |
|
Node C --------+-------- Node B

If hash("user:42") sits just after Node C and before Node A, the successor is Node A. Adding Node B on that same arc steals only keys in the new interval; keys already past Node B toward Node A stay on A.

Naive hashing would instead number nodes alphabetically and assign hash % 3 (or % 4 after the add), which can reshuffle most keys.

Complexity

Let N = physical nodes, V = virtual nodes (N × vnodeCount), K = keys.

OperationTime (this implementation)Space
Hash a stringO(length of string)O(1) extra
Build sorted ringO(V log V)O(V)
Find ownerO(V) linear scanO(1)
Insert / delete nodeRebuild ring + reassign all keys: O(V log V + K V)O(N + V + K)
Insert keyO(V) to assignO(1) amortized in the array
Redistribute all keysO(V log V + K V)O(K)
Migration diffO(K)O(K)
PreviewClone + same as topology changeExtra clone of nodes/keys
StatsO(N + K)O(N)

Virtual nodes increase V, which improves balance in practice but makes sort and successor scans more expensive. Key enter animations on the ring are skipped when K > 80 to keep the UI responsive.

Design Decisions

  • Circular ring matches the successor rule and makes wrap-around visible.
  • MD5 truncated to 32 bits is deterministic and cheap for a visualizer; it is not a cryptographic design for production clusters.
  • Linear successor keeps the code easy to test and to explain; the ring sizes here are pedagogical, not millions of vnodes.
  • Virtual nodes are first-class so students can see balance vs. vnode count (1–100).
  • In-memory Express state avoids a database for a demo; all clients share one ring on that server process.
  • Preview + apply in consistent mode separates “what would move?” from committing the change.
  • Parallel naive key list uses the same names/hashes so comparison is fair; synthetic vnode ids naive:name reuse the migration helper.
  • Reduced motion honors prefers-reduced-motion and an optional persisted toggle so education still works without long animations.

Edge Cases

Handled in the API and/or UI:

CaseBehavior
Empty ringKeys exist but assignedNode / assignedVirtualNode are null; lookup is rejected
Single nodeAll keys assign to that node (and its vnodes)
Duplicate node name409 — node already exists
Duplicate key name409 — key already exists
Remove missing node404
Remove last nodeAllowed; remaining keys become unassigned
Invalid vnode count400 — must be integer 1–100
Vnode count in naive mode400 — not applicable
Generate countInteger 1–5000; unique k_<n>_<hex> names
Hash wrap-aroundOwner is the first vnode in sorted order
Lookup vs assignment mismatch500 if tracer steps disagree with stored owner
Preview outside consistent mode400
Mode switchRemaps with the same keys/nodes; no migration animation

Equal vnode hashes are ordered by JavaScript’s stable sort after the numeric comparison; successor still uses the first hash >= keyHash.

Tests

cd server && npm testcd client && npm test

Server tests cover ring golden cases, stats, migration, preview, and naive comparison. Client tests cover histogram scaling, ring ownership helpers, migration motion, and lookup visuals.

Future Improvements

These are not implemented. They follow from how the visualizer works today (in-memory ring, linear successor scan, truncated MD5, shared process state).

Algorithm and scale

  • Faster successor lookup — replace the linear Array.find with binary search on the sorted ring (true O(log V) lookups).
  • Optional hash functions — let users compare MD5, SHA-1, or a simple FNV-style hash so they can see clustering vs. spread.
  • Weighted nodes — give a physical node extra virtual nodes to model a larger machine, instead of the same vnode count for every server.
  • Replication factor — show N clockwise successors (like a Dynamo-style preference list), not only the primary owner.

Simulation and pedagogy

  • Larger-scale demos — tens of thousands of keys with sampled animation, plus a table of expected vs. observed migration percent when N changes.
  • Failure / recovery walkthrough — a guided “node dies, then returns” sequence with freeze-frame ownership arcs.
  • Export / import scenarios — save a node+key setup as JSON so lessons can be replayed.

Product and operations

  • Per-client sessions — isolate rings so concurrent users (or tabs) do not share one in-memory cluster.
  • Persistence — optional store for sessions if the visualizer is hosted; the API currently resets on process restart.
  • Documented deploy + screenshots — a live URL and short GIFs of add-node, naive vs. consistent, and lookup trace.
  • A project LICENSE fileserver/package.json still carries npm’s default ISC string; a real license should be chosen explicitly.

Learning Resources

Contributing

  1. Fork or branch from main.
  2. Keep ring assignment logic in server/core and visualization in client/src.
  3. Add or update node --test files when you change hashing, migration, or ownership.
  4. Open a pull request with a short description of the behavior change.

Author

GitHub repository: devs-diaries/Consistent_Hashing_Visualizer

Organization: devs-diaries

Contributor: Aishwary_Malviya

About

Real-time interactive visualizer for Consistent Hashing with virtual nodes — great for learning and demos.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages