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
- 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.
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
- 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.
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
- Cannot write to closing transport
- data argument must be str (%r)
- data argument must be byte-ish (%r)
- %s() called while another coroutine is already waiting for i
- Concurrent call to receive() is not allowed
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/680362de11ce8bea.json.
Report an issue: GitHub.