redis/redis-py · error · RedisError

Cannot issue a WATCH after a MULTI

Error message

Cannot issue a WATCH after a MULTI

What it means

RedisError raised by TransactionStrategy._validate_watch (redis/asyncio/cluster.py:3202) when WATCH is called after MULTI was explicitly started. This mirrors core Redis semantics: WATCH after MULTI has no effect and is illegal. The library rejects it client-side before sending.

Solutions

  1. Call watch() before multi(): WATCH then MULTI then queued commands then EXEC
  2. Restructure so all WATCH calls happen first, then start the transaction with MULTI

Example fix

// before
pipe.multi()
await pipe.watch('k1')
// after
await pipe.watch('k1')
pipe.multi()
Defensive patterns

Strategy: validation

Validate before calling

# enforce ordering before building the pipeline
assert not pipe._explicit_transaction, 'call watch() before multi()'

Try / catch

try:
    await pipe.watch('k1')
except RedisError as e:
    if 'WATCH after a MULTI' in str(e):
        # reorder: reset, watch first, then multi

Prevention

When it happens

Trigger: `pipe.multi(); await pipe.watch('k1')` on a ClusterPipeline.

Common situations: Reordering pipeline setup code so MULTI precedes WATCH; copy-paste from a standalone flow that tolerated the ordering differently.

Related errors


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

Appendix: source

Thrown at redis/asyncio/cluster.py:3204

                    )

                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):
        if self._explicit_transaction:
            raise RedisError("Cannot issue a WATCH after a MULTI")

        self._watching = True

    async def _immediate_execute_command(self, *args, **options):
        return await self._retry.call_with_retry(
            lambda: self._get_connection_and_send_command(*args, **options),
            self._reinitialize_on_error,
            with_failure_count=True,
        )

    async def _get_connection_and_send_command(self, *args, **options):
        redis_node, connection = self._get_client_and_connection_for_transaction()
        # Only disconnect if not watching - disconnecting would lose WATCH state
        if not self._watching:
            await redis_node.disconnect_if_needed(connection)

        # Start timing for observability
        start_time = time.monotonic()

View on GitHub (pinned to 6a6b581b48)