{"id":"76d8a6188e63ef0d","repo":"redis/redis-py","slug":"watched-variable-changed-76d8a6","errorCode":null,"errorMessage":"Watched variable changed.","messagePattern":"Watched variable changed\\.","errorType":"exception","errorClass":"WatchError","httpStatus":null,"severity":"error","filePath":"redis/client.py","lineNumber":2061,"sourceCode":"                try:\n                    self.parse_response(connection, \"_\")\n                except ResponseError as e:\n                    self.annotate_exception(e, i + 1, command[0])\n                    errors.append((i, e))\n\n        # parse the EXEC.\n        try:\n            response = self.parse_response(connection, \"_\")\n        except ExecAbortError:\n            if errors:\n                raise errors[0][1]\n            raise\n\n        # EXEC clears any watched keys\n        self.watching = False\n\n        if response is None:\n            raise WatchError(\"Watched variable changed.\")\n\n        # put any parse errors into the response\n        for i, e in errors:\n            response.insert(i, e)\n\n        if len(response) != len(commands):\n            self.connection.disconnect()\n            raise ResponseError(\n                \"Wrong number of response items from pipeline execution\"\n            )\n\n        # find any errors in the response and raise if necessary\n        if raise_on_error:\n            self.raise_first_error(commands, response)\n\n        # We have to run response callbacks manually\n        data = []\n        for r, cmd in zip(response, commands):","sourceCodeStart":2043,"sourceCodeEnd":2079,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/client.py#L2043-L2079","documentation":"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.","triggerScenarios":"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.","commonSituations":"Optimistic locking on counters, inventory, or any read-modify-write; high contention where concurrent writers frequently invalidate each other's watches.","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."],"exampleFix":"# before\nwith client.pipeline() as pipe:\n    pipe.watch('counter')\n    pipe.multi()\n    pipe.incr('counter')\n    pipe.execute()  # raises if another writer touched 'counter'\n\n# after\nfor _ in range(5):\n    try:\n        with client.pipeline() as pipe:\n            pipe.watch('counter')\n            val = int(pipe.get('counter'))\n            pipe.multi()\n            pipe.set('counter', val + 1)\n            pipe.execute()\n        break\n    except WatchError:\n        continue","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"from redis.exceptions import WatchError\nfor _ in range(5):\n    try:\n        with client.pipeline() as pipe:\n            pipe.watch('k')\n            cur = int(pipe.get('k') or 0)\n            pipe.multi(); pipe.set('k', cur + 1)\n            pipe.execute()\n        break\n    except WatchError:\n        continue  # watched key changed; retry","preventionTips":["Treat WatchError as expected under contention and retry.","Minimize the WATCH->EXEC window.","Use server-side INCR/Lua for high-contention counters."],"tags":["pipeline","watch","transaction","optimistic-lock"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}