{"id":"1777297d646b788b","repo":"redis/redis-py","slug":"cannot-issue-nested-calls-to-multi","errorCode":null,"errorMessage":"Cannot issue nested calls to MULTI","messagePattern":"Cannot issue nested calls to MULTI","errorType":"exception","errorClass":"RedisError","httpStatus":null,"severity":"error","filePath":"redis/asyncio/client.py","lineNumber":1895,"sourceCode":"            # the pipeline must not be left holding a reference to a\n            # connection that is being returned to the pool. Shield the\n            # release itself so a second cancel cannot split the pool's\n            # internal in-use/available bookkeeping mid-update.\n            if self.connection:\n                connection, self.connection = self.connection, None\n                await asyncio.shield(self.connection_pool.release(connection))\n\n    async def aclose(self) -> None:\n        \"\"\"Alias for reset(), a standard method name for cleanup\"\"\"\n        await self.reset()\n\n    def multi(self):\n        \"\"\"\n        Start a transactional block of the pipeline after WATCH commands\n        are issued. End the transactional block with `execute`.\n        \"\"\"\n        if self.explicit_transaction:\n            raise RedisError(\"Cannot issue nested calls to MULTI\")\n        if self.command_stack:\n            raise RedisError(\n                \"Commands without an initial WATCH have already been issued\"\n            )\n        self.explicit_transaction = True\n\n    def execute_command(\n        self, *args, **kwargs\n    ) -> Union[\"Pipeline\", Awaitable[\"Pipeline\"]]:\n        if (self.watching or args[0] == \"WATCH\") and not self.explicit_transaction:\n            return self.immediate_execute_command(*args, **kwargs)\n        return self.pipeline_execute_command(*args, **kwargs)\n\n    async def _disconnect_reset_raise_on_watching(\n        self,\n        conn: Connection,\n        error: Exception,\n        failure_count: Optional[int] = None,","sourceCodeStart":1877,"sourceCodeEnd":1913,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/client.py#L1877-L1913","documentation":"Raised by Pipeline.multi() (redis/asyncio/client.py:1895) when self.explicit_transaction is already True. A transactional pipeline is begun with a single multi() call and terminated by execute()/discard(); calling multi() again is a programmer error. The library raises RedisError rather than silently nesting.","triggerScenarios":"Calling `pipe.multi()` twice on the same Pipeline object, e.g. in a loop that calls multi() per batch or in a helper that wraps multi() and is invoked twice.","commonSituations":"Reusing a pipeline across iterations and re-entering multi(); framework code that opens transactions conditionally and double-opens on retry.","solutions":["Call multi() exactly once per transaction; create a fresh pipeline via `client.pipeline(transaction=True)` for each transaction.","Let `client.pipeline(transaction=True)` auto-issue MULTI so you never call multi() manually.","Track explicit_transaction state and skip redundant multi() calls."],"exampleFix":"// before\npipe = r.pipeline()\npipe.multi()\npipe.multi()  # raises\n// after\npipe = r.pipeline(transaction=True)\n# MULTI issued automatically on execute; do not call multi()","handlingStrategy":"validation","validationCode":"pipe = r.pipeline()\nif getattr(pipe, 'explicit_transaction', False):\n    raise RuntimeError('pipeline already in a transaction')\npipe.multi()","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Call multi() at most once per pipeline; create a fresh pipeline per transaction.","Prefer client.pipeline(transaction=True) so MULTI is auto-managed.","Avoid helper layers that conditionally call multi()."],"tags":["redis","asyncio","pipeline","transactions","api-misuse"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}