aio-libs/aiohttp · error · RuntimeError

WebSocket connection is closed.

Error message

WebSocket connection is closed.

What it means

Raised by WebSocketResponse.receive() when the connection is already closed and receive() has been called more than THRESHOLD_CONNLOST_ACCESS (5) times after closing. The first few calls after close return the WS_CLOSED_MESSAGE sentinel; after the threshold the library raises to stop a runaway loop from spinning on a dead connection.

Source

Thrown at aiohttp/web_ws.py:610

    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):
                            msg = await self._reader.read()
                    else:
                        msg = await self._reader.read()
                finally:
                    self._waiting = False

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Break the receive loop when msg.type is WSMsgType.CLOSE, CLOSING, or CLOSED.
  2. Check `ws.closed` before/after receive and stop looping when True.
  3. Prefer `async for msg in ws` which raises StopAsyncIteration on terminal message types and exits cleanly.

Example fix

# before
while True:
    msg = await ws.receive()
    handle(msg)  # never breaks after peer closes

# after
async for msg in ws:
    if msg.type in (WSMsgType.TEXT, WSMsgType.BINARY):
        handle(msg)
# loop ends automatically on CLOSE/CLOSING/CLOSED
Defensive patterns

Strategy: validation

Validate before calling

async def receive_until_closed(ws):
    while not ws.closed:
        msg = await ws.receive()
        if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED):
            break
        yield msg

Type guard

def is_open(ws) -> bool:
    return not getattr(ws, "closed", True)

Try / catch

try:
    msg = await ws.receive()
except RuntimeError as e:
    if "WebSocket connection is closed" in str(e):
        # stop the loop; peer is gone
        return None
    raise

Prevention

When it happens

Trigger: A receive loop that keeps calling `await ws.receive()` after the peer closed and _closed is True. After 5 such calls (THRESHOLD_CONNLOST_ACCESS, defined at web_ws.py:63) the RuntimeError at line 610 fires instead of returning WS_CLOSED_MESSAGE.

Common situations: A `while True: msg = await ws.receive()` loop with no break on CLOSE/CLOSING/CLOSED message types; logic that ignores the closed sentinel and keeps polling; a bug where the exit condition is never met after disconnect.

Related errors


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