{"id":"ac9e32c6a8a94712","repo":"redis/redis-py","slug":"watched-variable-changed","errorCode":null,"errorMessage":"Watched variable changed.","messagePattern":"Watched variable changed\\.","errorType":"exception","errorClass":"WatchError","httpStatus":null,"severity":"warning","filePath":"redis/asyncio/client.py","lineNumber":2073,"sourceCode":"                try:\n                    await self.parse_response(connection, \"_\")\n                except ResponseError as err:\n                    self.annotate_exception(err, i + 1, command[0])\n                    errors.append((i, err))\n\n        # parse the EXEC.\n        try:\n            response = await self.parse_response(connection, \"_\")\n        except ExecAbortError as err:\n            if errors:\n                raise errors[0][1] from err\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.\") from None\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            if self.connection:\n                await self.connection.disconnect()\n            raise ResponseError(\n                \"Wrong number of response items from pipeline execution\"\n            ) from None\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 = []","sourceCodeStart":2055,"sourceCodeEnd":2091,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/client.py#L2055-L2091","documentation":"Raised by Pipeline._execute_transaction (redis/asyncio/client.py:2073) when the EXEC reply is None. In Redis, EXEC returns nil when a watched key was modified between WATCH and EXEC, aborting the transaction. redis-py surfaces this as WatchError ('Watched variable changed.') — the intended optimistic-concurrency signal.","triggerScenarios":"Standard OPTIMISTIC LOCKING contention: WATCH a key, read it, build a transaction, and another client modifies the key before EXEC runs. The server returns nil for EXEC and no queued commands execute.","commonSituations":"Concurrent writers racing on a shared key (counters, locks, read-modify-write); high contention where the watch window overlaps another client's commit.","solutions":["Retry the entire WATCH-read-modify-EXEC sequence on WatchError until it succeeds or a bound is hit.","Minimize the window between WATCH and EXEC (avoid await points on other I/O in between).","For hot keys, switch to an atomic primitive (INCR, Lua script, or SET NX) to avoid watch contention."],"exampleFix":"// before\npipe = r.pipeline(transaction=True)\nawait pipe.watch('k')\nval = int(await pipe.get('k'))\nawait pipe.multi()\nawait pipe.set('k', val + 1)\nawait pipe.execute()  # may raise WatchError\n// after\nfor _ in range(MAX_RETRIES):\n    try:\n        pipe = r.pipeline(transaction=True)\n        await pipe.watch('k')\n        val = int(await pipe.get('k'))\n        await pipe.multi()\n        await pipe.set('k', val + 1)\n        await pipe.execute()\n        break\n    except redis.exceptions.WatchError:\n        continue\n# or, contention-free: await r.incr('k')","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"from redis.exceptions import WatchError\nfor _ in range(MAX_RETRIES):\n    try:\n        pipe = r.pipeline(transaction=True)\n        await pipe.watch('k')\n        val = int(await pipe.get('k'))\n        await pipe.multi()\n        await pipe.set('k', val + 1)\n        await pipe.execute()\n        break\n    except WatchError:\n        continue","preventionTips":["Always retry the full WATCH-read-modify-EXEC loop on WatchError.","For contended counters, prefer INCR or a Lua script.","Minimize awaits between WATCH and EXEC."],"tags":["redis","asyncio","pipeline","transactions","watch","optimistic-locking","contention"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}