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() when a second concurrent receive is attempted. The _waiting flag is set True while a read() on the internal queue is in flight and cleared in a finally block. WebSocketResponse is not safe for concurrent receive(); only one coroutine may await a message at a time.

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 c0ef574e29)

Solutions

  1. Serialize all reads through a single consumer: have one loop call receive() and dispatch messages to other tasks via queues.
  2. Ensure only one task iterates the WebSocketResponse; remove any second receive()/async-for on the same object.
  3. If you need fan-out, feed received messages into an asyncio.Queue and have multiple workers pull from the queue.

Example fix

# before
# two tasks both call:
msg = await ws.receive()

# after
# single reader fans out to a queue
async def reader(ws, q):
    async for msg in ws:
        if msg.type == WSMsgType.TEXT:
            await q.put(msg.data)

async for data in consumer_queue:
    handle(data)
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()

Try / catch

try:
    msg = await ws.receive()
except RuntimeError as e:
    if "Concurrent call to receive()" in str(e):
        await asyncio.sleep(0)
        return await ws.receive()
    raise

Prevention

When it happens

Trigger: Two coroutines both awaiting `ws.receive()` (or `async for msg in ws`) on the same WebSocketResponse at the same time. The check at aiohttp/web_ws.py:604-605 sees `_waiting` already True.

Common situations: A background task calling receive() while the main handler loop also calls receive(); mixing manual receive() calls with `async for` iteration; a heartbeat task that tries to receive; spawning multiple readers with asyncio.gather on the same ws.

Related errors


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