Consistent Hashing is a technique used to distribute keys across multiple nodes while minimizing data movement when nodes are added or removed.
It is especially useful in distributed systems such as sharding, distributed caches, and database routing.
Why Do We Need It?
Suppose we have 3 database shards and use simple modulo hashing:
Shard = Hash(CustomerId) % 3
If we add a fourth shard:
Shard = Hash(CustomerId) % 4
A large number of customers may map to different shards. This means a significant amount of data may need to move.
flowchart LR
A[CustomerId] --> B[Hash % 3]
B --> C[Shard 1]
B --> D[Shard 2]
B --> E[Shard 3]
F[Add Shard 4] --> G[Hash % 4]
G --> H[Many Keys Remapped]
Consistent hashing reduces this movement.
Consistent Hashing Ring
Instead of directly mapping a key to a server using modulo, both keys and nodes are placed on a logical hash ring.
flowchart LR
A[Hash Ring]
A --> B[Shard 1]
A --> C[Shard 2]
A --> D[Shard 3]
E[Customer Key] --> F[Hash]
F --> G[Nearest Shard on Ring]
The important idea is:
Hash(CustomerId)
↓
Position on Ring
↓
Move clockwise
↓
First Shard Found
↓
Store / Read Data
Adding a New Shard
Suppose we initially have:
Shard 1
Shard 2
Shard 3
We add:
Shard 4
Only keys belonging to the affected ring range generally need to move.
flowchart LR
A[Before] --> B[Shard 1]
A --> C[Shard 2]
A --> D[Shard 3]
E[Add Shard 4] --> F[Only Affected Range]
F --> G[Shard 4]
Simple C# Implementation
Here is a simplified implementation to understand the routing idea.
using System.Security.Cryptography;
using System.Text;
public class ConsistentHashRing
{
private readonly SortedDictionary<uint, string> _ring = new();
public void AddNode(string node)
{
var hash = Hash(node);
_ring[hash] = node;
}
public void RemoveNode(string node)
{
var hash = Hash(node);
_ring.Remove(hash);
}
public string GetNode(string key)
{
if (_ring.Count == 0)
throw new InvalidOperationException("No nodes available.");
var hash = Hash(key);
foreach (var node in _ring)
{
if (node.Key >= hash)
return node.Value;
}
return _ring.First().Value;
}
private static uint Hash(string value)
{
using var sha256 = SHA256.Create();
var bytes = SHA256.HashData(
Encoding.UTF8.GetBytes(value));
return BitConverter.ToUInt32(bytes, 0);
}
}
ASP.NET Core Web API Example
Now let's connect consistent hashing with database routing.
sequenceDiagram
participant Client
participant API as ASP.NET Core Web API
participant Ring as Consistent Hash Ring
participant DB as SQL Server Shard
Client->>API: GET /api/customers/1500000/orders
API->>Ring: GetNode(CustomerId)
Ring-->>API: Shard-2
API->>DB: Query Orders
DB-->>API: Orders
API-->>Client: JSON Response
Shard Router
public interface IShardRouter
{
string GetConnectionString(long customerId);
}
public class ConsistentHashShardRouter : IShardRouter
{
private readonly ConsistentHashRing _ring = new();
private readonly IConfiguration _configuration;
public ConsistentHashShardRouter(
IConfiguration configuration)
{
_configuration = configuration;
_ring.AddNode("Shard1");
_ring.AddNode("Shard2");
_ring.AddNode("Shard3");
}
public string GetConnectionString(long customerId)
{
var shard = _ring.GetNode(customerId.ToString());
return _configuration
.GetConnectionString(shard)!;
}
}
Controller
[ApiController]
[Route("api/customers")]
public class OrdersController : ControllerBase
{
private readonly IShardRouter _router;
public OrdersController(IShardRouter router)
{
_router = router;
}
[HttpGet("{customerId:long}/orders")]
public async Task<IActionResult> GetOrders(
long customerId)
{
var connectionString =
_router.GetConnectionString(customerId);
await using var connection =
new SqlConnection(connectionString);
await connection.OpenAsync();
const string sql = """
SELECT OrderId,
CustomerId,
Amount,
OrderDate,
Status
FROM Orders
WHERE CustomerId = @CustomerId;
""";
await using var command =
new SqlCommand(sql, connection);
command.Parameters.Add(
"@CustomerId",
SqlDbType.BigInt).Value = customerId;
var orders = new List<object>();
await using var reader =
await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
orders.Add(new
{
OrderId = reader.GetInt64(0),
CustomerId = reader.GetInt64(1),
Amount = reader.GetDecimal(2),
OrderDate = reader.GetDateTime(3),
Status = reader.GetString(4)
});
}
return Ok(orders);
}
}
SQL Server Complete Example
The database schema remains the same on every shard.
Shard 1
CREATE DATABASE OrdersShard1;
GO
USE OrdersShard1;
GO
CREATE TABLE Orders
(
OrderId BIGINT PRIMARY KEY,
CustomerId BIGINT NOT NULL,
Amount DECIMAL(18,2) NOT NULL,
OrderDate DATE NOT NULL,
Status VARCHAR(30) NOT NULL
);
GO
INSERT INTO Orders
VALUES
(101, 1001, 1000.00, '2026-09-01', 'Completed'),
(102, 2001, 1500.00, '2026-09-02', 'Pending');
GO
Shard 2
CREATE DATABASE OrdersShard2;
GO
USE OrdersShard2;
GO
CREATE TABLE Orders
(
OrderId BIGINT PRIMARY KEY,
CustomerId BIGINT NOT NULL,
Amount DECIMAL(18,2) NOT NULL,
OrderDate DATE NOT NULL,
Status VARCHAR(30) NOT NULL
);
GO
INSERT INTO Orders
VALUES
(201, 3001, 2500.00, '2026-09-03', 'Completed'),
(202, 4001, 3500.00, '2026-09-04', 'Pending');
GO
Shard 3
CREATE DATABASE OrdersShard3;
GO
USE OrdersShard3;
GO
CREATE TABLE Orders
(
OrderId BIGINT PRIMARY KEY,
CustomerId BIGINT NOT NULL,
Amount DECIMAL(18,2) NOT NULL,
OrderDate DATE NOT NULL,
Status VARCHAR(30) NOT NULL
);
GO
INSERT INTO Orders
VALUES
(301, 5001, 4500.00, '2026-09-05', 'Completed'),
(302, 6001, 5500.00, '2026-09-06', 'Pending');
GO
Query a Specific Shard
Once the router determines the shard, the SQL query is normal:
SELECT
OrderId,
CustomerId,
Amount,
OrderDate,
Status
FROM Orders
WHERE CustomerId = 5001;
GO
Expected output:
OrderId CustomerId Amount OrderDate Status
------- ---------- ------- ---------- ----------
301 5001 4500.00 2026-09-05 Completed
appsettings.json
{
"ConnectionStrings": {
"Shard1": "Server=sql01;Database=OrdersShard1;...",
"Shard2": "Server=sql02;Database=OrdersShard2;...",
"Shard3": "Server=sql03;Database=OrdersShard3;..."
}
}
Register the Router
builder.Services.AddSingleton<IShardRouter,
ConsistentHashShardRouter>();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
Virtual Nodes
In real distributed systems, a single hash position per shard may not distribute traffic evenly.
A common improvement is virtual nodes.
Shard 1 → 100 virtual nodes
Shard 2 → 100 virtual nodes
Shard 3 → 100 virtual nodes
flowchart LR
A[Shard 1] --> B[Virtual Node 1]
A --> C[Virtual Node 2]
A --> D[Virtual Node 3]
E[Shard 2] --> F[Virtual Node 1]
E --> G[Virtual Node 2]
E --> H[Virtual Node 3]
This gives each physical shard multiple positions on the ring and generally improves distribution.
Consistent Hashing vs Modulo Hashing
| Modulo Hashing | Consistent Hashing |
|---|---|
Hash(key) % N |
Hash key onto a ring |
| Adding nodes can remap many keys | Usually only affected ranges move |
| Simple | More complex |
| Good for fixed node counts | Useful when nodes change dynamically |
Important Interview Point
Consistent hashing does not mean that no data moves when a node changes.
It means the amount of remapping can be significantly reduced compared with simple modulo hashing.
When Should You Use It?
- Large distributed systems.
- Dynamic shard environments.
- Distributed caching.
- Systems where nodes can be added or removed.
- Large-scale database routing.
Interview Scenario
Interviewer: You have 10 database shards and need to add an 11th shard. How can you avoid moving most of the data?
Answer: I could use consistent hashing. Instead of using Hash(key) % N, I would place shards and keys on a hash ring. When the new shard is added, only the keys in the affected ring range need to be redistributed. Virtual nodes can further improve distribution.
Common Interview Questions
- What is consistent hashing?
- Why is it better than simple modulo hashing when nodes change?
- What happens when a shard is added?
- What happens when a shard is removed?
- What is a hash ring?
- What are virtual nodes?
- How can consistent hashing be used for database sharding?
- How would ASP.NET Core route a request using consistent hashing?
Key Takeaways
- Consistent hashing distributes keys using a hash ring.
- It reduces data movement when nodes are added or removed.
- Virtual nodes improve distribution.
- It is useful for dynamic distributed systems.
- ASP.NET Core can use a consistent-hash router before accessing SQL Server.
- It is especially useful when the number of shards can change.
Conclusion
CustomerId
↓
Hash Function
↓
Consistent Hash Ring
↓
Shard
↓
SQL Server
↓
ASP.NET Core Response
Remember: modulo hashing is simple, but consistent hashing is useful when the number of nodes changes and minimizing data movement matters.