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

  1. Catch WatchError and retry the entire WATCH->read->MULTI->EXEC sequence, with a cap on attempts.
  2. Reduce the window between WATCH and EXEC (do minimal work inside the transaction).
  3. 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

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


AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04). Data as JSON: /data/errors/76d8a6188e63ef0d.json. Report an issue: GitHub.