redis/redis-py · error · RedisError

Cannot issue a WATCH after a MULTI

Error message

Cannot issue a WATCH after a MULTI

What it means

Raised by TransactionStrategy._validate_watch (redis/cluster.py:4704) when WATCH is called after MULTI has already started the transaction. Per Redis semantics WATCH inside a MULTI is illegal (Redis returns an error); the cluster client enforces this client-side. Once _explicit_transaction is True, no further WATCH is allowed.

Solutions

  1. Always call watch() before multi()/transactional queuing begins.
  2. Restructure so all watched keys are known up front, then start MULTI.
  3. If conditions change mid-transaction, discard and restart: DISCARD, re-WATCH, re-MULTI.

Example fix

// before
with rc.pipeline(transaction=True) as pipe:
    pipe.multi()
    pipe.watch('k')
    pipe.set('k', 'v')
// after
with rc.pipeline(transaction=True) as pipe:
    pipe.watch('k')
    pipe.multi()
    pipe.set('k', 'v')
Defensive patterns

Strategy: validation

Validate before calling

with rc.pipeline(transaction=True) as pipe:
    pipe.watch('k')   # WATCH BEFORE MULTI
    pipe.multi()
    pipe.set('k', 'v')

Type guard

def watch_before_multi(sequence) -> bool:
    return sequence.index('watch') < sequence.index('multi')

Try / catch

from redis.exceptions import RedisError
try:
    pipe.watch('k')
except RedisError as e:
    if 'Cannot issue a WATCH after a MULTI' in str(e):
        pipe.discard()
        pipe.watch('k')
        pipe.multi()

Prevention

When it happens

Trigger: In a transactional pipeline, calling pipe.multi() (or queuing commands that implicitly start MULTI) and then calling pipe.watch(key) afterward.

Common situations: Reordering WATCH/MULTI when porting standalone code; conditional logic that adds a watch late; misunderstanding that WATCH must precede MULTI.

Related errors


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

Appendix: source

Thrown at redis/cluster.py:4704

                    )

                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 self.pipeline_execute_command(*args, **kwargs)

    def _validate_watch(self):
        if self._explicit_transaction:
            raise RedisError("Cannot issue a WATCH after a MULTI")

        self._watching = True

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

    def _get_connection_and_send_command(self, *args, **options):
        redis_node, connection = self._get_client_and_connection_for_transaction()

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

        try:
            response = self._send_command_parse_response(

View on GitHub (pinned to 6a6b581b48)