aio-libs/aiohttp · error · RuntimeError

Concurrent call to receive() is not allowed

Error message

Concurrent call to receive() is not allowed

What it means

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.

Source

Thrown at aiohttp/client_ws.py:391

    @overload
    async def receive(
        self: "ClientWebSocketResponse[Literal[False]]", timeout: float | None = None
    ) -> WSMessageNoDecodeText: ...

    @overload
    async def receive(
        self: "ClientWebSocketResponse[_DecodeText]", timeout: float | None = None
    ) -> WSMessageDecodeText | WSMessageNoDecodeText: ...

    async def receive(
        self, timeout: float | None = None
    ) -> WSMessageDecodeText | WSMessageNoDecodeText:
        receive_timeout = timeout or self._timeout.ws_receive

        while True:
            if self._waiting:
                raise RuntimeError("Concurrent call to receive() is not allowed")

            if self._closed:
                return WS_CLOSED_MESSAGE
            elif self._closing:
                await self.close()
                return WS_CLOSED_MESSAGE

            try:
                self._waiting = True
                try:
                    if receive_timeout:
                        # Entering the context manager and creating
                        # Timeout() object can take almost 50% of the
                        # run time in this loop so we avoid it if
                        # there is no read timeout.
                        async with async_timeout.timeout(receive_timeout):
                            msg = await self._reader.read()
                    else:

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Ensure only one task calls receive() at a time; serialize reads through a single consumer loop.
  2. If multiple consumers are needed, dispatch received messages via an asyncio.Queue fed by a single reader task.
  3. Move heartbeat logic to use ws.ping() instead of receive().
  4. Guard with an asyncio.Lock if you cannot guarantee single-consumer discipline.

Example fix

# before
msg1, msg2 = await asyncio.gather(ws.receive(), ws.receive())
# after - single consumer
async def reader(ws, queue):
    async for msg in ws:
        await queue.put(msg)
# other tasks consume from `queue`
Defensive patterns

Strategy: validation

Validate before calling

import asyncio
read_lock = asyncio.Lock()
async def safe_receive(ws):
    async with read_lock:
        return await ws.receive()

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/680362de11ce8bea.json. Report an issue: GitHub.