skip to content

Multi-Key Operation Limits

You will learn why MGET, MULTI, and Lua scripts fail with CROSSSLOT in cluster mode unless every key lands in one slot, and how hash tags work around it at the cost of skew. Interviewers ask because this constraint reshapes key design in every clustered Redis system.

part ofRedisoverview, primer and where to startread it →
on this pageshow

questions

5

An MGET over three keys that worked against a single Redis instance now fails with "CROSSSLOT Keys in request don't hash to the same slot" after a move to Redis Cluster. Explain the rule being enforced and the realistic ways to make that read work.

level: middleimportance: must knowfreq 58%

answer

  1. one command, one slot - node executes locally
  2. CROSSSLOT is permanent, not a redirect
  3. slot equality, not node equality, is checked
  4. hash tag / split-and-pipeline / one hash + HMGET
  5. client-side split loses atomicity

basics

~20 s

In cluster mode every key of a single command must live in one hash slot, because one node executes it locally with no cross-node coordination. Fix it by hash-tagging the keys into one slot, by issuing separate GETs pipelined per node, or by restructuring the data into one key.

solid answer

~60 s

Redis Cluster executes each command entirely on the node that owns the slot; there is no distributed execution or two-phase coordination. So a multi-key command is only legal when **all** its keys hash to the same slot, and `MGET a b c` with keys in different slots is rejected up front with `CROSSSLOT`. Three honest options: 1. **Co-locate with hash tags** - rename to `{ctx}:a`, `{ctx}:b`, `{ctx}:c` so all three hash the same tag. Correct when the keys genuinely belong to one entity; dangerous if the tag is broad, because a slot cannot be split. 2. **Split client-side.** Send individual `GET`s, grouped per node and pipelined, so you still get roughly one round trip per node. Many clients (for example Lettuce) do this automatically for `MGET`/`MSET`/`DEL` - note it loses the single-command atomicity. 3. **Restructure.** If the fields always travel together, store them as one hash and use `HMGET`, which is a single-key command and always legal. The same rule governs `MSET`, `SINTERSTORE`, `RENAME`, `SMOVE`, `PFMERGE`, `BITOP`, `ZRANGESTORE` and friends.

code

text · 12 lines
text
> MGET user:1:name user:1:email user:1:plan
(error) CROSSSLOT Keys in request don't hash to the same slot

# fix A: co-locate with a hash tag (narrow identity)
> MGET "user:{1}:name" "user:{1}:email" "user:{1}:plan"
1) "ada"
2) "[email protected]"
3) "pro"

# fix B: model it as one object - single-key command, legal anywhere
> HSET user:1 name ada email [email protected] plan pro
> HMGET user:1 name email plan

go deeper

for a junior

State the rule - all keys of one command must be in one slot - and name the hash-tag fix.

for a middle

List the affected command families, explain the slot-not-node subtlety, and know that client-side splitting trades atomicity for reach.

for a senior

Drive the decision from requirements: atomic or not, co-location group bounded or not, and prefer restructuring over broad tags.

for a principal

Treat it as a data-model boundary rather than an error to suppress: define which access paths need co-location, encode that as a documented key-naming contract, and watch for split-write partial-failure semantics.

## The rule Redis Cluster shards the keyspace into 16384 hash slots, each owned by one master. A command is dispatched to a single node and executed there against local memory only. There is no cross-node query engine, no distributed transaction, no scatter-gather in the server. Therefore any command that names more than one key is accepted **only if every one of those keys hashes to the same slot**. Otherwise the node answers: ``` (error) CROSSSLOT Keys in request don't hash to the same slot ``` Note what this is not. It is not a redirect and not a temporary condition - it will never succeed by retrying, because the keys are permanently in different slots. And the check is on the **slot**, not the node: two keys that happen to sit on the same master today but in different slots are still rejected, precisely so that a later slot migration cannot silently break the command. ## Which commands are affected Everything that takes multiple key arguments, including the ones people forget: - Batching: `MGET`, `MSET`, `MSETNX`, `DEL`/`UNLINK`/`EXISTS`/`TOUCH` with several keys. - Set and sorted-set algebra with a destination: `SINTERSTORE`, `SUNIONSTORE`, `SDIFFSTORE`, `ZUNIONSTORE`, `ZINTERSTORE`, `ZDIFFSTORE`, `ZRANGESTORE`; also the non-storing `SINTER`, `SUNION`, `SDIFF`, `ZINTERCARD` over several keys. - Moves and copies: `RENAME`, `RENAMENX`, `COPY`, `SMOVE`, `LMOVE`, `BLMOVE`, `LMPOP`, `ZMPOP`, `BLMPOP`. - Merges and bit ops: `PFMERGE`, `PFCOUNT` over several keys, `BITOP`. - Reading several streams in one call: `XREAD`/`XREADGROUP` with multiple stream names. - `SORT` with `BY`/`GET` patterns that reference other keys, and `GEORADIUS ... STORE`. - `EVAL`/`FCALL` with keys in `KEYS` spanning slots, and multi-key `MULTI` blocks. ## Option 1: hash tags Rename the keys so they share a hash tag: `{order:77}:items`, `{order:77}:total`. Redis then hashes only `order:77`, so all of them land in one slot and the multi-key command becomes legal. This is the right answer when the keys are facets of one entity whose cardinality is high - millions of orders spread evenly over slots. It is the wrong answer when the tag is broad (`{tenant}`, `{prod}`), because a slot is indivisible: everything under one tag is pinned to one node forever, and resharding cannot relieve the resulting memory and CPU hotspot. Tag by the narrowest identity the operation actually spans. ## Option 2: split client-side For a *read* like `MGET`, atomicity is usually not required - you are fetching three independent values, and Redis gives you no snapshot isolation across commands anyway. So issue three `GET`s. The efficient shape is to group them by owning node using the client's cached slot map and pipeline each group: with a three-node cluster you pay three parallel round trips instead of one, not three sequential ones. Several mature clients do this transparently. Lettuce splits cross-slot `MGET`, `MSET` and `DEL` into per-node batches and reassembles the result in the caller's key order; redis-py's cluster client similarly fans out. Know whether *your* client does this, because the behavioural difference is important: the fan-out version is **not atomic**. Concurrent writers can change one key between the sub-requests, so you can observe a mix of old and new values. For a cache read that is almost always fine; for a read that must be self-consistent it is not, and you need option 1 or 3. Writes are more delicate. A split `MSET` can partially fail: some nodes accept, one is down, and you are left with half the batch applied and no rollback. Either make the write idempotent and retryable, or co-locate. ## Option 3: restructure the data Often the multi-key command is an artifact of a standalone-era schema. Three keys that are always read together are usually one object: store them as a hash and read with `HMGET`, which names one key and therefore works anywhere in the cluster. A set intersection over per-user sets can sometimes be replaced by a precomputed set maintained at write time. Restructuring removes the constraint instead of working around it, and it does not create slot skew. ## Choosing Ask two questions. *Does this operation need to be atomic or self-consistent?* If yes, co-location or a single-key structure is mandatory; splitting is not an option. *Is the co-location group bounded and numerous?* If yes, a hash tag is safe; if the group is unbounded (a tenant, an environment), prefer restructuring, because you would be trading a correctness error today for an unfixable hotspot later.

  • Your client library transparently splits a cross-slot MGET into per-node requests. What have you given up?
    Atomicity and point-in-time consistency. The sub-requests execute independently, so a concurrent writer can modify one key after another sub-request has already read its old value, and you can observe a mix of versions. Partial failure is also possible for split writes: one node's batch can fail while others succeed, with no rollback.
  • Two keys currently live on the same master node but in different slots. Will a multi-key command over them succeed?
    No. The check is on slot equality, not node co-residence, and it fails with CROSSSLOT. This is intentional: if the command were allowed, a later slot migration would move one key away and silently break an operation that had been working.

saying these in an interview costs you the question

  • Treating CROSSSLOT as transient and adding retries
  • Assuming keys on the same node can be used together even in different slots
  • Slapping a broad hash tag like {app} or {tenant} on everything to make the errors disappear
  • Believing a client-side split preserves the atomicity of MGET or MSET
  • Thinking Redis Cluster will internally scatter-gather the request across nodes

context

open as a page

What extra constraints does Redis Cluster impose on Lua scripts executed with EVAL and on transactions opened with MULTI, compared with running the same code against a single Redis instance?

level: seniorimportance: must knowfreq 46%

basics

~20 s

Both run entirely on one node. Every key a script or transaction touches must hash to one slot, all script keys must be declared in KEYS (Redis refuses undeclared or non-local key access), and a cross-slot command inside MULTI aborts the transaction.

open as a page

Which Redis commands and features behave differently, or stop working entirely, when an application moves from a single Redis instance to Redis Cluster mode?

level: middleimportance: should knowfreq 45%

basics

~20 s

Only database 0 exists, so SELECT beyond 0, SWAPDB and MOVE are gone. Multi-key commands, scripts and transactions must stay within one slot. KEYS, SCAN, DBSIZE, FLUSHALL and RANDOMKEY act per node and need fan-out. Classic Pub/Sub broadcasts cluster-wide unless you use sharded channels.

open as a page

A colleague proposes putting every key belonging to a customer inside a hash tag such as {tenant:42} so that multi-key commands and Lua scripts keep working after a move to Redis Cluster. What does that buy, and what are the operational risks?

level: seniorimportance: should knowfreq 42%

basics

~20 s

It buys legal multi-key commands and single-node atomicity per tenant. It costs balance: all of a tenant's data and traffic sit in one indivisible slot, so a large or busy tenant creates a hotspot that resharding cannot fix, since slots can be moved but never split.

open as a page

You are moving an application that leans heavily on multi-key Redis commands - set intersections, MSET batches, and Lua scripts touching several keys - onto Redis Cluster. How do you decide, per access path, between co-locating keys with hash tags, restructuring the data, or moving the work into the application?

level: principalimportance: should knowfreq 32%

basics

~20 s

Classify each access path by whether it truly needs atomicity. Paths that do get a narrow hash tag; paths that merely batch get split and pipelined per node; recurring groups get collapsed into one key. Reject any co-location group that is unbounded or uneven.

open as a page