redis/redis-py · error · WatchError
Watched variable changed.
Error message
Watched variable changed.
What it means
Raised in `Pipeline._execute_transaction` after EXEC returns. In an optimistic-locking transaction, if any watched key was modified between WATCH and EXEC, Redis returns nil for the EXEC result; the client detects `response is None` and raises `WatchError('Watched variable changed.')`. This is the expected signal that the transaction aborted and must be retried.
Source
Thrown at redis/client.py:2061
try:
self.parse_response(connection, "_")
except ResponseError as e:
self.annotate_exception(e, i + 1, command[0])
errors.append((i, e))
# parse the EXEC.
try:
response = self.parse_response(connection, "_")
except ExecAbortError:
if errors:
raise errors[0][1]
raise
# 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(commands):
self.connection.disconnect()
raise ResponseError(
"Wrong number of response items from pipeline execution"
)
# find any errors in the response and raise if necessary
if raise_on_error:
self.raise_first_error(commands, response)
# We have to run response callbacks manually
data = []
for r, cmd in zip(response, commands):View on GitHub (pinned to da03cdc7e8)
Solutions
- Catch WatchError and retry the entire WATCH->read->MULTI->EXEC sequence, with a cap on attempts.
- Reduce the window between WATCH and EXEC (do minimal work inside the transaction).
- For high-contention counters, consider a server-side INCR/Lua script instead of optimistic locking.
Example fix
# before
with client.pipeline() as pipe:
pipe.watch('counter')
pipe.multi()
pipe.incr('counter')
pipe.execute() # raises if another writer touched 'counter'
# after
for _ in range(5):
try:
with client.pipeline() as pipe:
pipe.watch('counter')
val = int(pipe.get('counter'))
pipe.multi()
pipe.set('counter', val + 1)
pipe.execute()
break
except WatchError:
continue Defensive patterns
Strategy: retry
Try / catch
from redis.exceptions import WatchError
for _ in range(5):
try:
with client.pipeline() as pipe:
pipe.watch('k')
cur = int(pipe.get('k') or 0)
pipe.multi(); pipe.set('k', cur + 1)
pipe.execute()
break
except WatchError:
continue # watched key changed; retry Prevention
- Treat WatchError as expected under contention and retry.
- Minimize the WATCH->EXEC window.
- Use server-side INCR/Lua for high-contention counters.
When it happens
Trigger: Standard WATCH/MULTI/EXEC flow where another client (or this one) modifies a watched key before EXEC runs. The library translates the server's nil EXEC into WatchError so the application can retry the atomic compare-and-set.
Common situations: Optimistic locking on counters, inventory, or any read-modify-write; high contention where concurrent writers frequently invalidate each other's watches.
Related errors
- method watch() is not supported outside of transactional con
- Cannot issue a WATCH after a MULTI
- 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
AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04).
Data as JSON: /data/errors/76d8a6188e63ef0d.json.
Report an issue: GitHub.