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
- Serialize all reads through a single consumer task — one `async for msg in ws:` loop, nothing else calling receive().
- If you must fan out, run one reader task that pushes messages into an asyncio.Queue and have workers read from that queue.
- Ensure prior receive() calls are not still awaiting (e.g. a cancelled task that did not unwind its finally clearing _waiting).
- 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
- Have exactly one task reading from the WebSocket; fan messages out through an asyncio.Queue.
- Never call receive_str/receive_bytes/receive_json from a different task than the main loop.
- Ensure a cancelled receive() unwinds fully (its finally clears _waiting) before starting another.
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
- WebSocket connection is closed.
- 1002
- Cannot call .write() for websocket
- Concurrent call to receive() is not allowed
- Received message : is not WSMsgType.BINARY
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)