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

CrossSlotTransactionError raised in TransactionStrategy._execute_command (redis/asyncio/cluster.py:3184) during the WATCH/immediate path. While watching, all commands must target the same slot as the already-registered pipeline slots; attempting to watch or run a command whose slot differs from the existing one breaks the single-node transaction guarantee. Redis Cluster transactions are only atomic within one hash slot.

Solutions

  1. Use hash tags to force keys into the same slot: 'user:{1}' and 'account:{1}'
  2. Split the work into separate single-slot transactions
  3. Switch to the non-atomic pipeline strategy if cross-slot operation is acceptable

Example fix

// before
await pipe.watch('user:1')
await pipe.watch('account:2')
// after - same slot via hash tag
await pipe.watch('user:{1}')
await pipe.watch('account:{1}')
Defensive patterns

Strategy: validation

Validate before calling

from redis.cluster import key_slot
slots = {key_slot(k.encode()) for k in watch_keys}
if len(slots) > 1:
    raise ValueError('watch keys map to different slots; use hash tags')

Try / catch

try:
    await pipe.watch(*keys)
except CrossSlotTransactionError:
    # rebucket keys with hash tags or split transactions

Prevention

When it happens

Trigger: `pipe.watch('user:1')` then `pipe.watch('user:2')` where the two keys hash to different slots; mixing a watched key with an immediate command on a different slot before MULTI.

Common situations: Forgetting to use hash tags ({tag}) when transacting over logically related keys; assuming cluster transactions behave like standalone transactions.

Related errors


AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10). Data as JSON: /api/errors/38bdd069a0735e8e. Report an issue: GitHub.

Appendix: source

Thrown at redis/asyncio/cluster.py:3184

    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._resolve_transaction_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 6a6b581b48)