redis/redis-py · error · CrossSlotTransactionError

Cannot watch or send commands on different slots

Error message

Cannot watch or send commands on different slots

What it means

Raised as CrossSlotTransactionError inside _execute_command when the pipeline is in WATCH or immediate-execute mode and a newly queued command's slot differs from the slots already accumulated in _pipeline_slots. Redis Cluster transactions are atomic only within one hash slot, so the library refuses to mix slots in a watched/immediate path.

Source

Thrown at redis/asyncio/cluster.py:3152

    async def _execute_command(
        self, *args: Union[KeyT, EncodableT], **kwargs: Any
    ) -> Any:
        if self._pipe.cluster_client._initialize:
            await self._pipe.cluster_client.initialize()

        slot_number: Optional[int] = None
        if args[0] not in self.NO_SLOTS_COMMANDS:
            slot_number = await self._pipe.cluster_client._determine_slot(*args)

        if (
            self._watching or args[0] in self.IMMEDIATE_EXECUTE_COMMANDS
        ) and not self._explicit_transaction:
            if args[0] == "WATCH":
                self._validate_watch()

            if slot_number is not None:
                if self._pipeline_slots and slot_number not in self._pipeline_slots:
                    raise CrossSlotTransactionError(
                        "Cannot watch or send commands on different slots"
                    )

                self._pipeline_slots.add(slot_number)
            elif args[0] not in self.NO_SLOTS_COMMANDS:
                raise RedisClusterException(
                    f"Cannot identify slot number for command: {args[0]},"
                    "it cannot be triggered in a transaction"
                )

            return self._immediate_execute_command(*args, **kwargs)
        else:
            if slot_number is not None:
                self._pipeline_slots.add(slot_number)

            return super().execute_command(*args, **kwargs)

    def _validate_watch(self):

View on GitHub (pinned to da03cdc7e8)

Solutions

  1. Ensure every key in the transaction shares a hash tag, e.g. 'user:{1}' and 'account:{1}' so they land in the same slot.
  2. Restructure so all watched + transacted keys belong to one slot; split multi-entity work into per-slot transactions.
  3. If cross-slot atomicity is truly required, move that workflow to a Redis that supports it (single instance, or functional-redis-style multi-key commands like Lua/MULTI on one shard) — not a cluster transaction.

Example fix

// before
await pipe.watch('user:1')
await pipe.get('user:2')  # different slot -> raises [81]
// after
await pipe.watch('user:{1}')
await pipe.get('acct:{1}')  # same slot via hash tag
Defensive patterns

Strategy: validation

Validate before calling

from redis.cluster import key_slot
slots = {key_slot(k.encode()) for k in keys_to_watch_or_use}
if len(slots) > 1:
    raise ValueError('All keys must share a slot; use hash tags')

Try / catch

from redis.exceptions import CrossSlotTransactionError
try:
    await pipe.execute()
except CrossSlotTransactionError:
    # re-bucket keys by hash tag and retry per-slot

Prevention

When it happens

Trigger: After WATCH('k1') (or after queuing a key command that fixed the transaction's slot), calling another immediate-execute command whose key hashes to a different slot, e.g. WATCH('user:1') then pipeline.get('user:2') where the two keys are not hash-tagged to the same slot.

Common situations: Forgetting to use Redis hash tags ({tag}) on keys that must be transacted together; mixing keys with different prefixes in a single WATCH/MULTI block; copying standalone-pipeline code into a cluster pipeline.

Related errors


AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04). Data as JSON: /data/errors/38bdd069a0735e8e.json. Report an issue: GitHub.