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 by WebSocketResponse.receive() at the top of its loop when self._waiting is already True. aiohttp tracks an in-flight read because the WebSocket has a single shared reader queue and frame ordering would be corrupted by two concurrent consumers. The flag is set just before awaiting reader.read() and cleared in the finally block.

Solutions

  1. Serialize all reads through a single consumer task — one `async for msg in ws:` loop, nothing else calling receive().
  2. If you must fan out, run one reader task that pushes messages into an asyncio.Queue and have workers read from that queue.
  3. Ensure prior receive() calls are not still awaiting (e.g. a cancelled task that did not unwind its finally clearing _waiting).
  4. Do not call receive_str/receive_bytes/receive_json from a different task than the main receive loop.

Example fix

// before
asyncio.gather(ws.receive(), ws.receive())  # raises in one

// after
async def reader(ws, out):
    async for msg in ws:
        await out.put(msg)
asyncio.create_task(reader(ws, queue))
# workers consume from `queue`, never call ws.receive()
Defensive patterns

Strategy: try-catch

Validate before calling

# There is no safe pre-check; the only valid design is a single reader.
# Run exactly one task that calls ws.receive() and routes results via an asyncio.Queue.

Type guard

def ws_is_idle(ws: web.WebSocketResponse) -> bool:
    return not getattr(ws, '_waiting', False)  # informational only; races in concurrent code

Try / catch

try:
    msg = await ws.receive()
except RuntimeError as e:
    if 'Concurrent call to receive' in str(e):
        # route this reader through a shared queue instead of retrying
        raise
    raise

Prevention

When it happens

Trigger: Two tasks both calling `await ws.receive()` (or receive_str/receive_bytes/receive_json, which all call receive()) at the same time; an `async for msg in ws:` loop running while another task also calls receive(); a heartbeat/error handler that reads while the main loop reads.

Common situations: Spawning a second asyncio task to 'monitor' the socket while the main loop iterates messages; calling receive() from both the request handler and a cleanup task; re-entering receive() recursively.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/1b15c07728bce956. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/web_ws.py:605

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

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

    async def receive(
        self, timeout: float | None = None
    ) -> WSMessageDecodeText | WSMessageNoDecodeText:
        if self._reader is None:
            raise RuntimeError("Call .prepare() first")

        receive_timeout = timeout or self._receive_timeout
        while True:
            if self._waiting:
                raise RuntimeError("Concurrent call to receive() is not allowed")

            if self._closed:
                self._conn_lost += 1
                if self._conn_lost >= THRESHOLD_CONNLOST_ACCESS:
                    raise RuntimeError("WebSocket connection is closed.")
                return WS_CLOSED_MESSAGE
            elif self._closing:
                return WS_CLOSING_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):

View on GitHub (pinned to d041d4d0fd)