redis/redis-py · warning · WatchError
Watched variable changed.
Error message
Watched variable changed.
What it means
Raised as WatchError after EXEC when the server returns nil for EXEC. A nil EXEC is Redis's signal that a watched key was modified between WATCH and EXEC, so the transaction was aborted and no commands ran. This is expected CAS-failure semantics, not a bug.
Source
Thrown at redis/asyncio/cluster.py:3423
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 da03cdc7e8)
Solutions
- Treat WatchError as a normal retry signal: re-read the value, re-enter WATCH/MULTI/EXEC, and loop until it commits.
- Cap the retry count and back off to avoid livelock under heavy contention.
- Reduce the WATCH-to-EXEC window and narrow the watched key set to lower collision probability.
Example fix
// before
await pipe.watch('counter')
v = await pipe.get('counter')
await pipe.multi(); await pipe.incr('counter'); await pipe.execute() # may raise [87]
// after
while True:
try:
await pipe.watch('counter')
v = int(await pipe.get('counter'))
await pipe.multi(); await pipe.incr('counter')
await pipe.execute(); break
except WatchError:
continue Defensive patterns
Strategy: retry
Validate before calling
# CAS retry budget — cannot prevent the first failure max_cas_retries = 10
Try / catch
from redis.exceptions import WatchError
for _ in range(max_cas_retries):
try:
await run_optimistic_lock(client)
break
except WatchError:
continue # watched key changed; retry the whole block Prevention
- Treat WatchError as expected and retry the whole WATCH/MULTI/EXEC block.
- Narrow the watched key set and shorten the WATCH-to-EXEC window.
When it happens
Trigger: Any cluster (or standalone) pipeline WATCH/MULTI/EXEC where another client (or another part of the same app) writes to a watched key before EXEC is processed.
Common situations: Optimistic-locking patterns (read-modify-write); high-contention keys; long intervals between WATCH and EXEC that raise the chance of a concurrent write.
Related errors
- Slot rebalancing occurred while watching keys
- A {type(error).__name__} occurred while watching one or more
- Cannot issue a WATCH after a MULTI
- A {type(error).__name__} occurred while watching one or more
- Watched variable changed.
AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04).
Data as JSON: /data/errors/db1b5174f27d812c.json.
Report an issue: GitHub.