{"id":"680362de11ce8bea","repo":"aio-libs/aiohttp","slug":"concurrent-call-to-receive-is-not-allowed","errorCode":null,"errorMessage":"Concurrent call to receive() is not allowed","messagePattern":"Concurrent call to receive\\(\\) is not allowed","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"aiohttp/client_ws.py","lineNumber":391,"sourceCode":"\n    @overload\n    async def receive(\n        self: \"ClientWebSocketResponse[Literal[False]]\", timeout: float | None = None\n    ) -> WSMessageNoDecodeText: ...\n\n    @overload\n    async def receive(\n        self: \"ClientWebSocketResponse[_DecodeText]\", timeout: float | None = None\n    ) -> WSMessageDecodeText | WSMessageNoDecodeText: ...\n\n    async def receive(\n        self, timeout: float | None = None\n    ) -> WSMessageDecodeText | WSMessageNoDecodeText:\n        receive_timeout = timeout or self._timeout.ws_receive\n\n        while True:\n            if self._waiting:\n                raise RuntimeError(\"Concurrent call to receive() is not allowed\")\n\n            if self._closed:\n                return WS_CLOSED_MESSAGE\n            elif self._closing:\n                await self.close()\n                return WS_CLOSED_MESSAGE\n\n            try:\n                self._waiting = True\n                try:\n                    if receive_timeout:\n                        # Entering the context manager and creating\n                        # Timeout() object can take almost 50% of the\n                        # run time in this loop so we avoid it if\n                        # there is no read timeout.\n                        async with async_timeout.timeout(receive_timeout):\n                            msg = await self._reader.read()\n                    else:","sourceCodeStart":373,"sourceCodeEnd":409,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/client_ws.py#L373-L409","documentation":"Raised as RuntimeError in ClientWebSocketResponse.receive() when self._waiting is already True at the top of the loop. receive() is a single-consumer API: only one outstanding read against the underlying transport is allowed at a time, because the WebSocket reader is stateful and concurrent reads would interleave frame halves.","triggerScenarios":"Fires at line 390-391 when receive() is called from two tasks concurrently before the first returns. Triggered by awaiting two receive() calls in parallel via asyncio.gather, or by calling receive() from one task while another is still awaiting it.","commonSituations":"Two coroutines reading the same ws connection; a heartbeat/ping task running receive() alongside the main consumer; refactoring that accidentally wraps receive() in gather; calling receive() from a re-entrant callback.","solutions":["Ensure only one task calls receive() at a time; serialize reads through a single consumer loop.","If multiple consumers are needed, dispatch received messages via an asyncio.Queue fed by a single reader task.","Move heartbeat logic to use ws.ping() instead of receive().","Guard with an asyncio.Lock if you cannot guarantee single-consumer discipline."],"exampleFix":"# before\nmsg1, msg2 = await asyncio.gather(ws.receive(), ws.receive())\n# after - single consumer\nasync def reader(ws, queue):\n    async for msg in ws:\n        await queue.put(msg)\n# other tasks consume from `queue`","handlingStrategy":"validation","validationCode":"import asyncio\nread_lock = asyncio.Lock()\nasync def safe_receive(ws):\n    async with read_lock:\n        return await ws.receive()","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Run a single reader task per connection; dispatch via asyncio.Queue.","Do not wrap receive() in asyncio.gather.","Use ping() for heartbeats rather than receive().","Add an asyncio.Lock if you cannot guarantee single-consumer access."],"tags":["websocket","concurrency","client-ws","asyncio"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}