{"id":"96c3282b1d7c462d","repo":"redis/redis-py","slug":"command-all-keys-must-map-to-the-same-key-slot","errorCode":null,"errorMessage":"{command} - all keys must map to the same key slot","messagePattern":"(.+?) - all keys must map to the same key slot","errorType":"exception","errorClass":"RedisClusterException","httpStatus":null,"severity":"error","filePath":"redis/asyncio/cluster.py","lineNumber":1027,"sourceCode":"                # FCALL can call a function with 0 keys, that means the function\n                #  can be run on any node so we can just return a random slot\n                if command.upper() in (\"FCALL\", \"FCALL_RO\"):\n                    return random.randrange(0, REDIS_CLUSTER_HASH_SLOTS)\n                raise RedisClusterException(\n                    \"No way to dispatch this command to Redis Cluster. \"\n                    \"Missing key.\\nYou can execute the command by specifying \"\n                    f\"target nodes.\\nCommand: {args}\"\n                )\n\n        # single key command\n        if len(keys) == 1:\n            return self.keyslot(keys[0])\n\n        # multi-key command; we need to make sure all keys are mapped to\n        # the same slot\n        slots = {self.keyslot(key) for key in keys}\n        if len(slots) != 1:\n            raise RedisClusterException(\n                f\"{command} - all keys must map to the same key slot\"\n            )\n\n        return slots.pop()\n\n    def _is_node_flag(self, target_nodes: Any) -> bool:\n        return isinstance(target_nodes, str) and target_nodes in self.node_flags\n\n    def _parse_target_nodes(self, target_nodes: Any) -> List[\"ClusterNode\"]:\n        if isinstance(target_nodes, list):\n            nodes = target_nodes\n        elif isinstance(target_nodes, ClusterNode):\n            # Supports passing a single ClusterNode as a variable\n            nodes = [target_nodes]\n        elif isinstance(target_nodes, dict):\n            # Supports dictionaries of the format {node_name: node}.\n            # It enables to execute commands with multi nodes as follows:\n            # rc.cluster_save_config(rc.get_primaries())","sourceCodeStart":1009,"sourceCodeEnd":1045,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/cluster.py#L1009-L1045","documentation":"Raised by _determine_slot() when a multi-key command's keys hash to more than one slot. Redis Cluster only permits multi-key operations (MSET, SUNIONSTORE, pipeline of writes, etc.) when every key lives in the same slot. The library computes keyslot() for each key and rejects the command client-side if the set of slots has length > 1, because the server would reject it with a CROSSSLOT error anyway.","triggerScenarios":"rc.mset({'k1':1,'k2':2}) where k1 and k2 hash to different slots; rc.smove('src','dst',v) with untagged keys; EVAL scripts that touch keys in different slots; any command touching >1 key without shared hash tags.","commonSituations":"Treating a cluster like standalone Redis and using cross-slot multi-key commands; migrating data with bulk MSET/DELETE; keys naturally named without a common {tag} prefix.","solutions":["Use Redis hash tags so all keys share a slot: name keys as '{user:1000}:name', '{user:1000}:email' — the substring inside {} is what gets hashed.","Fall back to per-key single-key operations (a loop of SET/GET) if the keys genuinely belong to different shards.","For LOAD or pipeline fan-out, ensure each grouped command's keys share a slot or split the group by slot before dispatch."],"exampleFix":"// before\nawait rc.mset({'k1': 1, 'k2': 2})\n\n// after\nawait rc.mset({'{tag}:k1': 1, '{tag}:k2': 2})","handlingStrategy":"validation","validationCode":"from redis.asyncio.cluster import RedisCluster\n\ndef assert_same_slot(rc: RedisCluster, *keys: str) -> None:\n    slots = {rc.keyslot(k) for k in keys}\n    if len(slots) != 1:\n        raise ValueError(\n            f'Keys {keys} map to multiple slots {slots}. Use a shared hash tag '\n            f'e.g. \"{{tag}}:k1\".'\n        )\n\n# usage before a multi-key op\nassert_same_slot(rc, 'k1', 'k2')\nawait rc.mset({'k1': 1, 'k2': 2})","typeGuard":"from redis.asyncio.cluster import RedisCluster\n\ndef keys_share_slot(rc: RedisCluster, keys: list[str]) -> bool:\n    if not keys:\n        return True\n    first = rc.keyslot(keys[0])\n    return all(rc.keyslot(k) == first for k in keys[1:])","tryCatchPattern":"from redis.cluster import RedisClusterException\n\ntry:\n    await rc.mset(mapping)\nexcept RedisClusterException as e:\n    if 'same key slot' in str(e):\n        # fall back to per-key writes across slots\n        async with rc.pipeline() as p:\n            for k, v in mapping.items():\n                p.set(k, v)\n            await p.execute()\n    else:\n        raise","preventionTips":["Design key names with a shared hash tag ({entity_id}:field) when they must be operated on together.","Validate multi-key command inputs with keyslot() before dispatch in test harnesses.","For bulk loads, partition keys by slot and group writes accordingly."],"tags":["redis-cluster","hash-slot","multi-key","cross-slot"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}