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 Pipeline.watch() when self.explicit_transaction is True — i.e. MULTI has already been issued on this connection. Redis forbids WATCH after MULTI (WATCH must precede MULTI); the library enforces the ordering client-side.

Solutions

  1. Move all WATCH calls before multi(): WATCH -> (optional reads) -> multi() -> commands -> execute().
  2. If you no longer need the watch, unwatch() before multi(); never watch() after.

Example fix

// before
pipe.multi()
await pipe.set('a', 1)
await pipe.watch('a')
// after
await pipe.watch('a')
pipe.multi()
await pipe.set('a', 1)
Defensive patterns

Strategy: validation

Validate before calling

assert not pipe.explicit_transaction, 'cannot WATCH after MULTI'
await pipe.watch('k')

Type guard

def can_watch(pipe) -> bool:
    return not pipe.explicit_transaction

Try / catch

try:
    await pipe.watch('k')
except RedisError as e:
    if 'WATCH after a MULTI' in str(e):
        # discard, restructure: WATCH first, then multi()
        await pipe.discard()
    else:
        raise

Prevention

When it happens

Trigger: Calling pipe.multi() then await pipe.watch('k'); or issuing WATCH inside the MULTI/EXEC body by mistake.

Common situations: Reordering WATCH and MULTI during refactoring; conditionally adding WATCH inside a block that already started a transaction.

Related errors


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

Appendix: source

Thrown at redis/asyncio/client.py:2300

                network_peer_port=getattr(conn, "port", None),
                error_type=e,
                retry_attempts=actual_retry_attempts,
                is_internal=False,
            )
            raise
        finally:
            await self.reset()

    async def discard(self):
        """Flushes all previously queued commands
        See: https://redis.io/commands/DISCARD
        """
        await self.execute_command("DISCARD")

    async def watch(self, *names: KeyT):
        """Watches the values at keys ``names``"""
        if self.explicit_transaction:
            raise RedisError("Cannot issue a WATCH after a MULTI")
        return await self.execute_command("WATCH", *names)

    async def unwatch(self):
        """Unwatches all previously specified keys"""
        return self.watching and await self.execute_command("UNWATCH") or True

View on GitHub (pinned to 6a6b581b48)