← Python Code FastAPI
Browse Python Concepts

WebSocket Basics in FastAPI — Connection Leakage

Mental Model

A WebSocket connection is a fragile, two-way channel that can be abruptly severed by either client or server. If the client disconnects, the server-side loop, unaware of the broken pipe, will eventually try to write, resulting in an error because the underlying network connection is no longer there.

Rule: Always wrap WebSocket communication loops in a try-except block to capture WebSocketDisconnect and close resources cleanly.

The Setup

You want to transmit a real-time system metrics stream to a dashboard. You set up a WebSocket endpoint in FastAPI that loops indefinitely to transmit health status packets at scheduled intervals.

What Does This Print?

Broken code
Python
from fastapi import FastAPI, WebSocket
import asyncio

app = FastAPI()

@app.websocket("/ws/metrics")
async def get_metrics(websocket: WebSocket):
    await websocket.accept()
    while True:
        # Simulating telemetry retrieval
        metrics = {"cpu": 22.4, "memory": 65.1}
        # Sending payload to the websocket client
        await websocket.send_json(metrics)
        await asyncio.sleep(1)
Predict what happens to the server process when a connected browser client closes the tab.

The Output

What actually happens
RuntimeError: Unexpected ASGI message 'websocket.send', after sending 'websocket.close' or response already completed.

The server logs a runtime traceback error and continues executing the infinite loop in the background: The socket is closed on the client side, but the route function process remains active and continues trying to send data, consuming server resources.

Why Python Does This

FastAPI WebSockets rely on Starlette's WebSocket implementation. When a client closes the network connection, Starlette updates its connection state, but it cannot prematurely kill the running Python coroutine. The next time the loop calls await websocket.send_json(), Starlette raises a WebSocketDisconnect exception (or subsequent RuntimeError). If this exception is not caught within the route function block, the coroutine execution frame remains suspended or repeatedly spawns errors in the ASGI runner thread, leaking memory.

The Fix

Corrected pattern
Python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import asyncio

app = FastAPI()

@app.websocket("/ws/metrics")
async def get_metrics(websocket: WebSocket):
    await websocket.accept()
    try:
        while True:
            metrics = {"cpu": 22.4, "memory": 65.1}
            await websocket.send_json(metrics)
            await asyncio.sleep(1)
    except WebSocketDisconnect:
        # Clean shutdown when client disconnects
        print("Client disconnected from metrics channel.")
    finally:
        # Run teardown and close connection explicitly if still active
        pass

Wrapping the WebSocket communication loop (while True) in a try...except WebSocketDisconnect block allows the server to gracefully catch the exception that occurs when the client closes the connection. This enables the server to break out of the loop and perform any necessary cleanup, preventing further attempts to send data to a non-existent socket.

How This Fails in Real Systems

A crypto tracking app broadcasted prices to hundreds of real-time charts. Due to uncaught disconnect exceptions, terminated user sessions remained active as zombie loop threads on the server, eventually exhausting the server's CPU capacity and causing API crashes.

Key Takeaway

Always wrap WebSocket communication loops in a try-except block to capture WebSocketDisconnect and close resources cleanly.
Common mistake: Not explicitly handling WebSocketDisconnect exceptions within an infinite WebSocket communication loop, leading to the server attempting to send data over a closed connection.