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
- Move all WATCH calls before multi(): WATCH -> (optional reads) -> multi() -> commands -> execute().
- 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
- Always issue WATCH before multi().
- Use the context-manager pipeline to keep ordering correct.
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
- Cannot issue a WATCH after a MULTI
- Commands without an initial WATCH have already been issued
- A occurred while watching one or more keys
- A occurred while watching one or more keys
- All keys involved in a cluster transaction must map to the…
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)