{"record":{"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/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/client_ws.py#L373-L409","documentation":"Raised inside ClientWebSocketResponse.receive() the moment a second coroutine enters the method while self._waiting is still True. aiohttp's websocket reader is single-threaded by design: it drains one frame at a time from one reader future, so a concurrent receive would corrupt that state. The guard makes the violation explicit instead of silently returning frames to the wrong caller.","triggerScenarios":"Two tasks both awaiting ws.receive(), ws.receive_str(), ws.receive_bytes(), or ws.receive_json() on the same ClientWebSocketResponse; a receive() that was never awaited still pending when another receive() starts; calling receive() from one task while close() or a previous receive is mid-flight in another.","commonSituations":"Fan-out processing where each worker tries to read the shared socket; mixing a background heartbeat task with a foreground reader; forgetting to await a receive() coroutine before starting another; reusing one ws across asyncio.gather branches.","solutions":["Designate exactly one task as the reader and dispatch frames to workers via an asyncio.Queue.","Serialize all receive calls behind a single asyncio.Lock (only one consumer acquires it at a time).","Cancel or await the previous receive() before starting a new one.","Move close()/ping() orchestration out of any task that also calls receive()."],"exampleFix":"# before\nawait asyncio.gather(ws.receive(), ws.receive())\n# after\nrecv_lock = asyncio.Lock()\nasync def safe_receive():\n    async with recv_lock:\n        return await ws.receive()","handlingStrategy":"validation","validationCode":"class SingleReceiver:\n    def __init__(self, ws):\n        self._ws = ws\n        self._lock = asyncio.Lock()\n    async def receive(self, timeout=None):\n        async with self._lock:\n            return await self._ws.receive(timeout)\n\nreader = SingleReceiver(ws)\n# all tasks call reader.receive(...), never ws.receive directly","typeGuard":"null","tryCatchPattern":"try:\n    msg = await ws.receive()\nexcept RuntimeError as exc:\n    if 'Concurrent call' in str(exc):\n        log.warning('receive serialized externally; retrying on the queue')\n        await frame_queue.put(('retry', None))\n    else:\n        raise","preventionTips":["Designate a single reader task and feed frames to an asyncio.Queue.","Never share one ClientWebSocketResponse across asyncio.gather branches that both read.","If multiple consumers are unavoidable, wrap receive() in an asyncio.Lock."],"tags":["websocket","client","concurrency","runtime-error"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}