{"record":{"id":"db1b5174f27d812c","repo":"redis/redis-py","slug":"watched-variable-changed-db1b51","errorCode":null,"errorMessage":"Watched variable changed.","messagePattern":"Watched variable changed\\.","errorType":"exception","errorClass":"WatchError","httpStatus":null,"severity":"warning","filePath":"redis/asyncio/cluster.py","lineNumber":3455,"sourceCode":"                    self._annotate_exception(e, i + 1, command.args)\n                    errors.append(e)\n\n        response = None\n        # parse the EXEC.\n        try:\n            response = await redis_node.parse_response(connection, \"EXEC\")\n        except ExecAbortError:\n            if errors:\n                raise errors[0]\n            raise\n\n        self._executing = False\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(self._command_queue):\n            raise InvalidPipelineStack(\n                \"Unexpected response length for cluster pipeline EXEC.\"\n                \" Command stack was {} but response had length {}\".format(\n                    [c.args[0] for c in self._command_queue], len(response)\n                )\n            )\n\n        # find any errors in the response and raise if necessary\n        if raise_on_error or len(errors) > 0:\n            await self._raise_first_error(\n                response,\n                self._command_queue,","sourceCodeStart":3437,"sourceCodeEnd":3473,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/asyncio/cluster.py#L3437-L3473","documentation":"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.","triggerScenarios":"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.","commonSituations":"Concurrent writers to the same key; CAS (compare-and-set) patterns under contention.","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"],"exampleFix":"// before\nawait pipe.watch('k1'); pipe.multi(); ...; await pipe.execute()\n// after\nwhile True:\n    try:\n        await pipe.watch('k1'); pipe.multi(); ...; await pipe.execute(); break\n    except WatchError:\n        await pipe.reset()","handlingStrategy":"retry","validationCode":"# intrinsic to WATCH; cannot prevent - must retry on contention","typeGuard":null,"tryCatchPattern":"from redis.exceptions import WatchError\nfor _ in range(MAX_RETRIES):\n    try:\n        await pipe.watch('k1'); pipe.multi(); ...; await pipe.execute(); break\n    except WatchError:\n        await pipe.reset()","preventionTips":["Always loop on WatchError for CAS patterns","Shorten the WATCH-to-EXEC window to reduce contention","Consider redis.lock for high-contention critical sections"],"tags":["cluster","pipeline","transaction","watch","optimistic-locking"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}