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

Concern Implementation
Hashing MD5 truncated to 32 bits (server/core/hash.js)
Ring Array of virtual nodes, sorted by hash, linear successor scan (Array.find)
Nodes { id, name, hash, virtualNodes[] }
Keys { key, hash, assignedNode, assignedVirtualNode }
Virtual nodes name#index; count 1–100, default 3
State Module-level arrays on the Express router (not a database)
Migration Diff on assignedVirtualNode.id; new keys are not counted as topology migrations
Preview Deep clone (structuredClone), mutate clone, redistribute, return before/after
Visualization SVG ring; angle = (hash / 0xffffffff) * 2π; Framer Motion + rAF walks
Client state React useState / useRef; Axios to VITE_API_URL or http://localhost:5000/api/ring
Tests Node.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

Technology Purpose
React 19 UI
Vite 7 Client bundler and dev server
Tailwind CSS 4 Styling
Axios HTTP client
Framer Motion Ring / key motion
Express 5 REST API
cors Cross-origin requests from the Vite app
dotenv Optional PORT loading
Node.js crypto MD5 hashing and random key names
Node.js test runner Unit / 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:

Variable Where Purpose
PORT Server HTTP port (default 5000)
VITE_API_URL Client (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.

Operation Time (this implementation) Space
Hash a string O(length of string) O(1) extra
Build sorted ring O(V log V) O(V)
Find owner O(V) linear scan O(1)
Insert / delete node Rebuild ring + reassign all keys: O(V log V + K V) O(N + V + K)
Insert key O(V) to assign O(1) amortized in the array
Redistribute all keys O(V log V + K V) O(K)
Migration diff O(K) O(K)
Preview Clone + same as topology change Extra clone of nodes/keys
Stats O(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:

Case Behavior
Empty ring Keys exist but assignedNode / assignedVirtualNode are null; lookup is rejected
Single node All keys assign to that node (and its vnodes)
Duplicate node name 409 — node already exists
Duplicate key name 409 — key already exists
Remove missing node 404
Remove last node Allowed; remaining keys become unassigned
Invalid vnode count 400 — must be integer 1–100
Vnode count in naive mode 400 — not applicable
Generate count Integer 1–5000; unique k_<n>_<hex> names
Hash wrap-around Owner is the first vnode in sorted order
Lookup vs assignment mismatch 500 if tracer steps disagree with stored owner
Preview outside consistent mode 400
Mode switch Remaps 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 test
cd 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