{"id":"be9174c9b270c96f","repo":"aio-libs/aiohttp","slug":"websocket-connection-is-closed","errorCode":null,"errorMessage":"WebSocket connection is closed.","messagePattern":"WebSocket connection is closed\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"aiohttp/web_ws.py","lineNumber":610,"sourceCode":"    async def receive(\n        self: \"WebSocketResponse[_DecodeText]\", timeout: float | None = None\n    ) -> WSMessageDecodeText | WSMessageNoDecodeText: ...\n\n    async def receive(\n        self, timeout: float | None = None\n    ) -> WSMessageDecodeText | WSMessageNoDecodeText:\n        if self._reader is None:\n            raise RuntimeError(\"Call .prepare() first\")\n\n        receive_timeout = timeout or self._receive_timeout\n        while True:\n            if self._waiting:\n                raise RuntimeError(\"Concurrent call to receive() is not allowed\")\n\n            if self._closed:\n                self._conn_lost += 1\n                if self._conn_lost >= THRESHOLD_CONNLOST_ACCESS:\n                    raise RuntimeError(\"WebSocket connection is closed.\")\n                return WS_CLOSED_MESSAGE\n            elif self._closing:\n                return WS_CLOSING_MESSAGE\n\n            try:\n                self._waiting = True\n                try:\n                    if receive_timeout:\n                        # Entering the context manager and creating\n                        # Timeout() object can take almost 50% of the\n                        # run time in this loop so we avoid it if\n                        # there is no read timeout.\n                        async with async_timeout.timeout(receive_timeout):\n                            msg = await self._reader.read()\n                    else:\n                        msg = await self._reader.read()\n                finally:\n                    self._waiting = False","sourceCodeStart":592,"sourceCodeEnd":628,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/web_ws.py#L592-L628","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Break the receive loop when msg.type is WSMsgType.CLOSE, CLOSING, or CLOSED.","Check `ws.closed` before/after receive and stop looping when True.","Prefer `async for msg in ws` which raises StopAsyncIteration on terminal message types and exits cleanly."],"exampleFix":"# before\nwhile True:\n    msg = await ws.receive()\n    handle(msg)  # never breaks after peer closes\n\n# after\nasync for msg in ws:\n    if msg.type in (WSMsgType.TEXT, WSMsgType.BINARY):\n        handle(msg)\n# loop ends automatically on CLOSE/CLOSING/CLOSED","handlingStrategy":"validation","validationCode":"async def receive_until_closed(ws):\n    while not ws.closed:\n        msg = await ws.receive()\n        if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED):\n            break\n        yield msg","typeGuard":"def is_open(ws) -> bool:\n    return not getattr(ws, \"closed\", True)","tryCatchPattern":"try:\n    msg = await ws.receive()\nexcept RuntimeError as e:\n    if \"WebSocket connection is closed\" in str(e):\n        # stop the loop; peer is gone\n        return None\n    raise","preventionTips":["Break receive loops on CLOSE/CLOSING/CLOSED message types.","Prefer `async for msg in ws` which terminates cleanly.","Check `ws.closed` before re-entering a loop."],"tags":["websocket","server","lifecycle","receive","closed"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}