Redis Cluster: Sharding, Hashing, and Replicas
To scale an application to handle 1 million writes per second, relying on a single-instance database is a critical bottleneck. Even an in-memory database like Redis, which is highly optimized and single-threaded, typically caps out at around 100,000 to 200,000 operations per second on standard hardware.
To break past this physical limitation, we must transition from a single-instance architecture to a distributed Redis Cluster. This transition requires a fundamental shift in how we generate unique identifiers, how data is distributed across physical memory, and how client drivers route requests.
1. The Scaling Bottleneck of Single-Threaded Redis
A single Redis instance runs its core execution loop on a single thread. While this design avoids context switching and locking overhead, it introduces a hard performance ceiling:
- Throughput Cap: A single instance typically maxes out between 100k and 200k reads/writes per second.
- Resource Underutilization: On modern multi-core server hardware (e.g., 32, 64, or 128 cores), a single-threaded Redis process leaves the vast majority of CPU capacity completely idle.
- The Solution: Run multiple Redis processes (instances) across the machine's cores and cluster them together. By running 30 distinct Redis processes on a single high-spec machine, we can parallelize workloads and scale throughput linearly to hit the 1-million-request milestone.
2. Eliminating Sequential ID Bottlenecks with UUIDs
In a naive, single-instance database setup, unique IDs are often generated sequentially (e.g., 1, 2, 3...) or managed via a centralized counter in Redis (using commands like INCR).
In a high-throughput environment processing 1 million requests per second, this sequential approach introduces a severe bottleneck:
- Every write requires a prior, blocking round-trip to Redis to increment and fetch the next unique ID.
- This creates a single point of contention across all parallel application processes.
The Ultra-Fast Approach: Decentralized UUIDs
To bypass this bottleneck, we shift ID generation entirely to the application layer using cryptographically secure, random 122-bit UUIDs (crypto.randomUUID()). This allows each application process to generate IDs instantly in memory without network overhead.
The Birthday Paradox & Collision Math
A common concern with random ID generation is the risk of collisions (generating the same ID twice). We can calculate the mathematical probability of a collision using the Birthday Paradox formula:
Where:
- is Euler's number ()
- is the total possible search space ( for a 122-bit UUID)
- is the number of generated IDs
If we continuously generate IDs at a rate of 1,000,000 writes per second, it would take 86,000 years of continuous operation to reach just a 50% probability of a single collision.
Note: Because the math is incredibly robust, we can safely eliminate ID-uniqueness checks in our hot path. For persistent storage, we can let the downstream relational database (e.g., PostgreSQL) handle auto-incrementing primary keys asynchronously during background synchronization.
3. Redis Cluster Architecture: Sharding, Slots, and Replicas
When running Redis in Cluster mode, data is automatically partitioned across multiple physical nodes. This is achieved using hash slots.
Key Architectural Concepts
- Hash Slots: A Redis Cluster always has exactly 16,384 logical hash slots. The physical master nodes split these slots among themselves.
- Master Nodes: Handle all write operations and active key management for their assigned hash slots.
- Replica Nodes: Maintain exact, asynchronous copies of their assigned master nodes. If a master node crashes, its replica is automatically promoted to master, ensuring high availability.
- Read/Write Split: Writes are directed to Master nodes. Reads can be optionally configured to target Replica nodes to further scale read throughput.
4. Hash Tags: Controlling Data Placement
In a standard Redis setup, you can write any key anywhere. In a Redis Cluster, the client or node must determine which physical node holds a given key. Redis does this by hashing the key:
The Multi-Key Problem in Clusters
If you perform operations involving multiple keys (such as transactions, pipelines, or queue operations), Redis requires all those keys to reside on the same physical node. If the keys hash to different slots on different nodes, the operation fails with a CROSSSLOT Keys in request don't hash to the same slot error.
The Solution: Hash Tags {}
To force specific keys to land on the exact same Redis node, use hash tags. When Redis detects curly braces {} in a key, it hashes only the content inside the braces to determine the slot.
// Without Hash Tags (Keys may land on different nodes):
const queueKey = "queue:user_12345";
const dataKey = "data:user_12345";
// With Hash Tags (Both keys are guaranteed to land on the same node):
const queueKey = "queue:{user_12345}";
const dataKey = "data:{user_12345}";
5. Code Implementation: Standalone vs. Cluster Mode
Below is a comparison of how the application code evolves to support high-throughput clustering.
Naive Standalone Implementation
This approach relies on sequential IDs and a single Redis connection, capping performance.
// standalone-server.js
import express from 'express';
import Redis from 'ioredis';
const app = express();
const redis = new Redis({ host: '127.0.0.1', port: 6379 });
app.post('/write-naive', async (req, res) => {
// Blocking sequential ID generation
const nextId = await redis.incr('global:id:counter');
const payload = { id: nextId, status: 'pending', timestamp: Date.now() };
// Single instance write
await redis.rpush('queue:requests', JSON.stringify(payload));
res.status(202).send({ success: true, id: nextId });
});
Decoupled Cluster Implementation (Ultra-Fast)
This optimized version uses decentralized UUID generation, cluster connection pooling, and hash tags to guarantee correct slot routing.
// cluster-server.js
import express from 'express';
import Redis from 'ioredis';
import crypto from 'crypto';
const app = express();
// Initialize Redis Cluster connection
const isClusterMode = process.env.REDIS_CLUSTER === 'true';
let redisClient;
if (isClusterMode) {
// Provide seed nodes; the driver auto-discovers the rest of the 30-node cluster
redisClient = new Redis.Cluster([
{ host: '127.0.0.1', port: 7000 },
{ host: '127.0.0.1', port: 7001 }
]);
console.log("Redis Cluster Connection Initialized.");
} else {
redisClient = new Redis({ host: '127.0.0.1', port: 6379 });
console.log("Redis Standalone Connection Initialized.");
}
app.post('/write-ultra-fast', async (req, res) => {
// 1. Generate a decentralized 122-bit random UUID (No network round-trip)
const jobId = crypto.randomUUID();
// 2. Define a shard key to control data placement.
// Using '{shard_1}' forces these writes to a specific node's slot,
// preventing CROSSSLOT errors during multi-key operations.
const shardTag = "shard_1";
const queueKey = `queue:{${shardTag}}`;
const dataKey = `job:{${shardTag}}:${jobId}`;
const payload = {
id: jobId,
status: 'enqueued',
timestamp: Date.now()
};
// 3. Execute writes to the cluster
await redisClient.hset(dataKey, 'data', JSON.stringify(payload));
await redisClient.rpush(queueKey, jobId);
res.status(202).send({ success: true, id: jobId });
});
app.listen(3000, () => console.log("Server running on port 3000"));
6. Complete End-to-End Request Flow
The diagram below illustrates how traffic flows from the load generator through the application processes and into the Redis Cluster.
7. Summary and Key Takeaways
- Single-Threaded Limitations: A single Redis instance caps out at 100k–200k operations/sec. Scaling to 1 million requests/sec requires running multiple instances in a cluster.
- Decentralized ID Generation: Moving from sequential database-managed counters to local, cryptographically secure UUIDs (
crypto.randomUUID()) removes a major network bottleneck. - The Birthday Paradox: Generating random 122-bit UUIDs at a rate of 1 million/sec would take 86,000 years to reach a 50% probability of a single collision.
- Hash Slots: Redis Clusters partition data across 16,384 logical hash slots distributed among master nodes.
- Hash Tags
{}: Placing curly braces around a specific portion of a key forces Redis to hash only that portion, ensuring related keys land on the same physical node and preventingCROSSSLOTerrors. - High Availability: A production-grade Redis Cluster pairs each Master node with a Replica node to ensure automatic failover and data redundancy.