redis/redis-py · warning · WatchError
Watched variable changed.
Error message
Watched variable changed.
What it means
WatchError raised in TransactionStrategy._execute_transaction (redis/asyncio/cluster.py:3455) when EXEC returns nil. A nil EXEC means Redis aborted the transaction because a watched key was modified between WATCH and EXEC by another client. This is the normal optimistic-locking signal - the application must retry the whole WATCH/transaction sequence.
Solutions
- Retry the full WATCH/MULTI/EXEC loop on WatchError until it succeeds or a bound is hit
- Reduce the window between WATCH and EXEC (do minimal work there)
- Consider a distributed lock (redis.lock) instead of WATCH if contention is high
Example fix
// before
await pipe.watch('k1'); pipe.multi(); ...; await pipe.execute()
// after
while True:
try:
await pipe.watch('k1'); pipe.multi(); ...; await pipe.execute(); break
except WatchError:
await pipe.reset() Defensive patterns
Strategy: retry
Validate before calling
# intrinsic to WATCH; cannot prevent - must retry on contention
Try / catch
from redis.exceptions import WatchError
for _ in range(MAX_RETRIES):
try:
await pipe.watch('k1'); pipe.multi(); ...; await pipe.execute(); break
except WatchError:
await pipe.reset() Prevention
- Always loop on WatchError for CAS patterns
- Shorten the WATCH-to-EXEC window to reduce contention
- Consider redis.lock for high-contention critical sections
When it happens
Trigger: Another client/connection writes to a watched key after your WATCH but before your EXEC completes; the watched value changed out from under the transaction.
Common situations: Concurrent writers to the same key; CAS (compare-and-set) patterns under contention.
Related errors
- Cannot identify slot number for command
- Cannot issue a WATCH after a MULTI
- Cannot issue a WATCH after a MULTI
- Cannot watch or send commands on different slots
- Cannot watch or send commands on different slots
AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10).
Data as JSON: /api/errors/db1b5174f27d812c.
Report an issue: GitHub.
Appendix: source
Thrown at redis/asyncio/cluster.py:3455
self._annotate_exception(e, i + 1, command.args)
errors.append(e)
response = None
# parse the EXEC.
try:
response = await redis_node.parse_response(connection, "EXEC")
except ExecAbortError:
if errors:
raise errors[0]
raise
self._executing = False
# EXEC clears any watched keys
self._watching = False
if response is None:
raise WatchError("Watched variable changed.")
# put any parse errors into the response
for i, e in errors:
response.insert(i, e)
if len(response) != len(self._command_queue):
raise InvalidPipelineStack(
"Unexpected response length for cluster pipeline EXEC."
" Command stack was {} but response had length {}".format(
[c.args[0] for c in self._command_queue], len(response)
)
)
# find any errors in the response and raise if necessary
if raise_on_error or len(errors) > 0:
await self._raise_first_error(
response,
self._command_queue,View on GitHub (pinned to 6a6b581b48)