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
- Call watch() before multi(): WATCH then MULTI then queued commands then EXEC
- 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
- Always call WATCH before MULTI
- Encapsulate the watch/multi/exec sequence in a helper that enforces ordering
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
- All keys involved in a cluster transaction must map to the…
- Cannot identify slot number for command
- Cannot issue a WATCH after a MULTI
- Cannot watch or send commands on different slots
- method multi() is not supported outside of transactional…
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)