DEV Community

Tim Nguyen
Tim Nguyen

Posted on

Redis Cluster Explained: Hash Slots, CROSSSLOT Errors, Lua Scripts, and Pipelining

If you've used standalone Redis and then switched to Redis Cluster, you may have encountered this error:

CROSSSLOT Keys in request don't hash to the same slot
Enter fullscreen mode Exit fullscreen mode

Why does the same code work locally but fail in Redis Cluster?

Let's understand how Redis distributes keys and why certain operations require keys to share the same hash slot.

1. What Are Redis Hash Slots?

Standalone Redis stores all keys on a single server. Redis Cluster distributes them across multiple primary nodes using 16,384 hash slots.

Each key is assigned a slot using:

HASH_SLOT = CRC16(key) % 16384
Enter fullscreen mode Exit fullscreen mode

For example (illustrative slot numbers):

Redis Cluster
|
+-- Node A (Slots 0-5460)
|   +-- user:1
|
+-- Node B (Slots 5461-10922)
|   +-- user:2
|
+-- Node C (Slots 10923-16383)
    +-- user:3
Enter fullscreen mode Exit fullscreen mode

Why use hash slots instead of assigning keys directly to nodes?

Scalability and rebalancing.

When adding or removing nodes, Redis can migrate hash slots between nodes without recalculating every key's placement.

A slot can contain millions or even billions of keys, subject to memory and Redis keyspace limits. However, one slot always belongs to one primary node at a time.

2. Why Does CROSSSLOT Happen?

Consider a Lua script that increments a topic's score while preventing repeated increments using a cooldown key.

if redis.call('EXISTS', KEYS[2]) == 1 then
  return 0
end

redis.call('ZINCRBY', KEYS[1], 1, ARGV[1])
redis.call('SET', KEYS[2], '1', 'EX', 30)

return 1
Enter fullscreen mode Exit fullscreen mode

With these keys:

KEYS[1] = app:topics:ranking
KEYS[2] = app:topics:cooldown:123
Enter fullscreen mode Exit fullscreen mode

Both keys may belong to different hash slots.

Redis Cluster cannot execute a Lua script atomically across multiple slots, so it rejects the operation with CROSSSLOT.

Solution: Hash Tags

Redis supports hash tags using {}.

When a key contains a valid hash tag, Redis hashes only the content inside the braces.

app:{topics}:ranking
app:{topics}:cooldown:123
Enter fullscreen mode Exit fullscreen mode

Both keys now hash using topics, guaranteeing the same slot.

You can verify this:

CLUSTER KEYSLOT app:{topics}:ranking
CLUSTER KEYSLOT app:{topics}:cooldown:123
Enter fullscreen mode Exit fullscreen mode

Both return the same slot number.

No changes to the Lua script are necessary.

3. What About MULTI/EXEC and Pipelines?

Redis Cluster has different restrictions depending on the operation.

Operation Different hash slots allowed? Atomic?
Independent GET / SET Yes Individually
MGET / MSET No Yes
MULTI/EXEC No Yes
EVAL (Lua) No Yes
ioredis cluster.pipeline() Only if slots share a node No
ioredis automatic pipelining Yes, across nodes No

MULTI/EXEC

Transactions require all keys to share the same slot.

// May fail with CROSSSLOT
await redis
  .multi()
  .set('user:1', 'John')
  .set('user:2', 'Jane')
  .exec();
Enter fullscreen mode Exit fullscreen mode

Using hash tags:

await redis
  .multi()
  .set('{users}:1', 'John')
  .set('{users}:2', 'Jane')
  .exec();
Enter fullscreen mode Exit fullscreen mode

Pipeline

Unlike transactions, pipelines are not atomic. They reduce network round trips by batching commands.

With ioredis, an explicit cluster pipeline requires its commands to target slots served by the same node.

const pipeline = redis.pipeline();

pipeline.set('user:1', 'John');
pipeline.set('user:2', 'Jane');

await pipeline.exec(); // May fail across nodes
Enter fullscreen mode Exit fullscreen mode

For independent operations across nodes, use automatic pipelining:

const redis = new Redis.Cluster(nodes, {
  enableAutoPipelining: true,
});

await Promise.all([
  redis.set('user:1', 'John'),
  redis.set('user:2', 'Jane'),
]);
Enter fullscreen mode Exit fullscreen mode

ioredis automatically groups eligible commands by destination node.

4. Be Careful With Hash Tags

Hash tags solve CROSSSLOT, but overusing them can create performance bottlenecks.

Consider:

app:{topics}:ranking
app:{topics}:cooldown:1
app:{topics}:cooldown:2
app:{topics}:cooldown:3
...
app:{topics}:cooldown:1000000
Enter fullscreen mode Exit fullscreen mode

All these keys belong to one hash slot, meaning one primary node handles them all.

Even if you add more Redis nodes, Redis cannot distribute a single slot across multiple primaries.

For user-specific operations, a better design is:

user:{123}:profile
user:{123}:settings

user:{456}:profile
user:{456}:settings
Enter fullscreen mode Exit fullscreen mode

Each user's related keys share a slot, while different users can be distributed across the cluster.

If you need an atomic operation involving one global key and many user-specific keys, you'll need to accept that shared-slot trade-off or redesign the operation.

5. Why Doesn't This Happen With Local Redis?

Standalone Redis doesn't enforce cluster hash-slot restrictions because all keys are stored on one server.

However, a local Redis Cluster with only one primary node still enforces hash-slot rules, even though all 16,384 slots belong to the same node.

If production uses Redis Cluster, consider testing against cluster mode locally to catch these problems early.

Conclusion

Redis Cluster uses 16,384 hash slots to distribute keys and simplify scaling.

Remember:

  • Hash slots determine where keys are stored.
  • Lua scripts and MULTI/EXEC require all keys to share the same slot.
  • Hash tags {} guarantee that related keys share a slot.
  • Pipelines improve performance but are not atomic.
  • Overusing hash tags can concentrate traffic on a single node.

The next time you encounter CROSSSLOT, don't just add {} to every key. Consider which operations truly require atomicity and how your key design affects scalability.


References

Top comments (0)