aio-libs/aiohttp · error · RuntimeError

WebSocket connection is closed.

Error message

WebSocket connection is closed.

What it means

Raised inside receive() once self._conn_lost reaches THRESHOLD_CONNLOST_ACCESS (=5). After the connection is marked closed, each receive() call increments _conn_lost and returns WS_CLOSED_MESSAGE up to 4 times; on the 5th call aiohttp raises to stop a caller from busy-looping receive() on a dead socket.

Solutions

  1. Break your receive loop when msg.type is WSMsgType.CLOSED, CLOSING, CLOSE, or ERROR.
  2. Check `ws.closed` before re-entering receive().
  3. After close, create a new WebSocketResponse for a new connection rather than reusing the closed one.
  4. Handle WS_CLOSED_MESSAGE explicitly instead of treating it as data.

Example fix

// before
while True:
    msg = await ws.receive()  # raises after 5th call post-close
    process(msg)

// after
while True:
    msg = await ws.receive()
    if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED, WSMsgType.ERROR):
        break
    process(msg)
Defensive patterns

Strategy: validation

Validate before calling

while not ws.closed:
    msg = await ws.receive()
    if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED, WSMsgType.ERROR):
        break
    handle(msg)

Type guard

def ws_is_dead(ws: web.WebSocketResponse) -> bool:
    return ws.closed  # property: self._closed

Try / catch

try:
    msg = await ws.receive()
except RuntimeError as e:
    if 'connection is closed' in str(e):
        break  # stop looping on the dead socket
    raise

Prevention

When it happens

Trigger: Application keeps calling `await ws.receive()` (or iterating) after the peer has disconnected and the socket is closed; a loop that ignores WS_CLOSED_MESSAGE / WS_CLOSING_MESSAGE and re-enters receive().

Common situations: A `while True: msg = await ws.receive()` loop with no break on msg.type in (CLOSE, CLOSING, CLOSED, ERROR); a retry loop that swallows the closed sentinel; reconnect logic that reuses the dead WebSocketResponse.

Related errors


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

Appendix: 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 d041d4d0fd)