[{"data":1,"prerenderedAt":1604},["ShallowReactive",2],{"post-en-websockets-fastapi":3},{"id":4,"title":5,"body":6,"date":1590,"description":17,"excerpt":1591,"extension":1592,"meta":1593,"navigation":116,"path":1594,"readTime":141,"seo":1595,"slug":1596,"stem":1597,"tags":1598,"__hash__":1603},"en_blog/en/blog/websockets-fastapi.md","WebSockets: Shared State, Authentication, and Disconnections",{"type":7,"value":8,"toc":1578},"minimark",[9,14,18,23,77,80,84,87,424,435,439,446,449,454,581,584,883,887,890,988,991,995,998,1131,1141,1145,1148,1245,1251,1280,1284,1289,1292,1489,1496,1549,1556,1560,1574],[10,11,13],"h1",{"id":12},"websockets-with-fastapi-shared-state-authentication-and-clean-disconnections","WebSockets with FastAPI: Shared State, Authentication, and Clean Disconnections",[15,16,17],"p",{},"The official FastAPI WebSocket tutorial fits in twenty lines. That is sufficient to understand the API, not to build something reliable. Here is what the examples do not show: authentication, distributed state management, broadcasting, and dead connection cleanup.",[19,20,22],"h2",{"id":21},"the-problem-with-the-basic-example","The Problem with the Basic Example",[24,25,30],"pre",{"className":26,"code":27,"language":28,"meta":29,"style":29},"language-python shiki shiki-themes github-dark github-light","# What every tutorial shows\n@app.websocket(\"/ws\")\nasync def websocket_endpoint(websocket: WebSocket):\n    await websocket.accept()\n    while True:\n        data = await websocket.receive_text()\n        await websocket.send_text(f\"Message: {data}\")\n","python","",[31,32,33,41,47,53,59,65,71],"code",{"__ignoreMap":29},[34,35,38],"span",{"class":36,"line":37},"line",1,[34,39,40],{},"# What every tutorial shows\n",[34,42,44],{"class":36,"line":43},2,[34,45,46],{},"@app.websocket(\"/ws\")\n",[34,48,50],{"class":36,"line":49},3,[34,51,52],{},"async def websocket_endpoint(websocket: WebSocket):\n",[34,54,56],{"class":36,"line":55},4,[34,57,58],{},"    await websocket.accept()\n",[34,60,62],{"class":36,"line":61},5,[34,63,64],{},"    while True:\n",[34,66,68],{"class":36,"line":67},6,[34,69,70],{},"        data = await websocket.receive_text()\n",[34,72,74],{"class":36,"line":73},7,[34,75,76],{},"        await websocket.send_text(f\"Message: {data}\")\n",[15,78,79],{},"This code has several production problems: no authentication, no handling of unexpected disconnections, no broadcasting to other clients, and state that does not survive a pod restart.",[19,81,83],{"id":82},"connectionmanager-handling-multiple-clients","ConnectionManager: Handling Multiple Clients",[15,85,86],{},"The first step is a manager that maintains the list of active connections:",[24,88,90],{"className":26,"code":89,"language":28,"meta":29,"style":29},"import asyncio\nfrom fastapi import WebSocket\nfrom typing import Any\nimport logging\n\nlogger = logging.getLogger(__name__)\n\nclass ConnectionManager:\n    def __init__(self):\n        # user_id -> list of websockets (a user may have multiple tabs open)\n        self._connections: dict[str, list[WebSocket]] = {}\n        self._lock = asyncio.Lock()\n\n    async def connect(self, websocket: WebSocket, user_id: str) -> None:\n        await websocket.accept()\n        async with self._lock:\n            if user_id not in self._connections:\n                self._connections[user_id] = []\n            self._connections[user_id].append(websocket)\n        logger.info(f\"WebSocket connected: user={user_id}, total={self.total_connections}\")\n\n    async def disconnect(self, websocket: WebSocket, user_id: str) -> None:\n        async with self._lock:\n            if user_id in self._connections:\n                self._connections[user_id] = [\n                    ws for ws in self._connections[user_id] if ws != websocket\n                ]\n                if not self._connections[user_id]:\n                    del self._connections[user_id]\n        logger.info(f\"WebSocket disconnected: user={user_id}\")\n\n    async def send_to_user(self, user_id: str, message: dict) -> None:\n        \"\"\"Sends a message to all connections belonging to a given user.\"\"\"\n        connections = self._connections.get(user_id, [])\n        dead_connections = []\n\n        for websocket in connections:\n            try:\n                await websocket.send_json(message)\n            except Exception:\n                dead_connections.append(websocket)\n\n        for ws in dead_connections:\n            await self.disconnect(ws, user_id)\n\n    async def broadcast(self, message: dict, exclude_user: str | None = None) -> None:\n        \"\"\"Broadcasts a message to all connected users.\"\"\"\n        tasks = []\n        for user_id in list(self._connections.keys()):\n            if user_id != exclude_user:\n                tasks.append(self.send_to_user(user_id, message))\n        await asyncio.gather(*tasks, return_exceptions=True)\n\n    @property\n    def total_connections(self) -> int:\n        return sum(len(ws_list) for ws_list in self._connections.values())\n\nmanager = ConnectionManager()\n",[31,91,92,97,102,107,112,118,123,127,133,139,145,151,157,162,168,174,180,186,192,198,204,209,215,220,226,232,238,244,250,256,262,267,273,279,285,291,296,302,308,314,320,326,331,337,343,348,354,360,366,372,378,384,390,395,401,407,413,418],{"__ignoreMap":29},[34,93,94],{"class":36,"line":37},[34,95,96],{},"import asyncio\n",[34,98,99],{"class":36,"line":43},[34,100,101],{},"from fastapi import WebSocket\n",[34,103,104],{"class":36,"line":49},[34,105,106],{},"from typing import Any\n",[34,108,109],{"class":36,"line":55},[34,110,111],{},"import logging\n",[34,113,114],{"class":36,"line":61},[34,115,117],{"emptyLinePlaceholder":116},true,"\n",[34,119,120],{"class":36,"line":67},[34,121,122],{},"logger = logging.getLogger(__name__)\n",[34,124,125],{"class":36,"line":73},[34,126,117],{"emptyLinePlaceholder":116},[34,128,130],{"class":36,"line":129},8,[34,131,132],{},"class ConnectionManager:\n",[34,134,136],{"class":36,"line":135},9,[34,137,138],{},"    def __init__(self):\n",[34,140,142],{"class":36,"line":141},10,[34,143,144],{},"        # user_id -> list of websockets (a user may have multiple tabs open)\n",[34,146,148],{"class":36,"line":147},11,[34,149,150],{},"        self._connections: dict[str, list[WebSocket]] = {}\n",[34,152,154],{"class":36,"line":153},12,[34,155,156],{},"        self._lock = asyncio.Lock()\n",[34,158,160],{"class":36,"line":159},13,[34,161,117],{"emptyLinePlaceholder":116},[34,163,165],{"class":36,"line":164},14,[34,166,167],{},"    async def connect(self, websocket: WebSocket, user_id: str) -> None:\n",[34,169,171],{"class":36,"line":170},15,[34,172,173],{},"        await websocket.accept()\n",[34,175,177],{"class":36,"line":176},16,[34,178,179],{},"        async with self._lock:\n",[34,181,183],{"class":36,"line":182},17,[34,184,185],{},"            if user_id not in self._connections:\n",[34,187,189],{"class":36,"line":188},18,[34,190,191],{},"                self._connections[user_id] = []\n",[34,193,195],{"class":36,"line":194},19,[34,196,197],{},"            self._connections[user_id].append(websocket)\n",[34,199,201],{"class":36,"line":200},20,[34,202,203],{},"        logger.info(f\"WebSocket connected: user={user_id}, total={self.total_connections}\")\n",[34,205,207],{"class":36,"line":206},21,[34,208,117],{"emptyLinePlaceholder":116},[34,210,212],{"class":36,"line":211},22,[34,213,214],{},"    async def disconnect(self, websocket: WebSocket, user_id: str) -> None:\n",[34,216,218],{"class":36,"line":217},23,[34,219,179],{},[34,221,223],{"class":36,"line":222},24,[34,224,225],{},"            if user_id in self._connections:\n",[34,227,229],{"class":36,"line":228},25,[34,230,231],{},"                self._connections[user_id] = [\n",[34,233,235],{"class":36,"line":234},26,[34,236,237],{},"                    ws for ws in self._connections[user_id] if ws != websocket\n",[34,239,241],{"class":36,"line":240},27,[34,242,243],{},"                ]\n",[34,245,247],{"class":36,"line":246},28,[34,248,249],{},"                if not self._connections[user_id]:\n",[34,251,253],{"class":36,"line":252},29,[34,254,255],{},"                    del self._connections[user_id]\n",[34,257,259],{"class":36,"line":258},30,[34,260,261],{},"        logger.info(f\"WebSocket disconnected: user={user_id}\")\n",[34,263,265],{"class":36,"line":264},31,[34,266,117],{"emptyLinePlaceholder":116},[34,268,270],{"class":36,"line":269},32,[34,271,272],{},"    async def send_to_user(self, user_id: str, message: dict) -> None:\n",[34,274,276],{"class":36,"line":275},33,[34,277,278],{},"        \"\"\"Sends a message to all connections belonging to a given user.\"\"\"\n",[34,280,282],{"class":36,"line":281},34,[34,283,284],{},"        connections = self._connections.get(user_id, [])\n",[34,286,288],{"class":36,"line":287},35,[34,289,290],{},"        dead_connections = []\n",[34,292,294],{"class":36,"line":293},36,[34,295,117],{"emptyLinePlaceholder":116},[34,297,299],{"class":36,"line":298},37,[34,300,301],{},"        for websocket in connections:\n",[34,303,305],{"class":36,"line":304},38,[34,306,307],{},"            try:\n",[34,309,311],{"class":36,"line":310},39,[34,312,313],{},"                await websocket.send_json(message)\n",[34,315,317],{"class":36,"line":316},40,[34,318,319],{},"            except Exception:\n",[34,321,323],{"class":36,"line":322},41,[34,324,325],{},"                dead_connections.append(websocket)\n",[34,327,329],{"class":36,"line":328},42,[34,330,117],{"emptyLinePlaceholder":116},[34,332,334],{"class":36,"line":333},43,[34,335,336],{},"        for ws in dead_connections:\n",[34,338,340],{"class":36,"line":339},44,[34,341,342],{},"            await self.disconnect(ws, user_id)\n",[34,344,346],{"class":36,"line":345},45,[34,347,117],{"emptyLinePlaceholder":116},[34,349,351],{"class":36,"line":350},46,[34,352,353],{},"    async def broadcast(self, message: dict, exclude_user: str | None = None) -> None:\n",[34,355,357],{"class":36,"line":356},47,[34,358,359],{},"        \"\"\"Broadcasts a message to all connected users.\"\"\"\n",[34,361,363],{"class":36,"line":362},48,[34,364,365],{},"        tasks = []\n",[34,367,369],{"class":36,"line":368},49,[34,370,371],{},"        for user_id in list(self._connections.keys()):\n",[34,373,375],{"class":36,"line":374},50,[34,376,377],{},"            if user_id != exclude_user:\n",[34,379,381],{"class":36,"line":380},51,[34,382,383],{},"                tasks.append(self.send_to_user(user_id, message))\n",[34,385,387],{"class":36,"line":386},52,[34,388,389],{},"        await asyncio.gather(*tasks, return_exceptions=True)\n",[34,391,393],{"class":36,"line":392},53,[34,394,117],{"emptyLinePlaceholder":116},[34,396,398],{"class":36,"line":397},54,[34,399,400],{},"    @property\n",[34,402,404],{"class":36,"line":403},55,[34,405,406],{},"    def total_connections(self) -> int:\n",[34,408,410],{"class":36,"line":409},56,[34,411,412],{},"        return sum(len(ws_list) for ws_list in self._connections.values())\n",[34,414,416],{"class":36,"line":415},57,[34,417,117],{"emptyLinePlaceholder":116},[34,419,421],{"class":36,"line":420},58,[34,422,423],{},"manager = ConnectionManager()\n",[15,425,426,427,430,431,434],{},"The ",[31,428,429],{},"asyncio.Lock()"," protects dictionary modifications — in Python, dict operations are not safe across concurrent coroutines. A user may have several simultaneous connections (multiple open tabs), which the ",[31,432,433],{},"dict[str, list[WebSocket]]"," structure handles natively.",[19,436,438],{"id":437},"authenticating-a-websocket-connection","Authenticating a WebSocket Connection",[15,440,441,442,445],{},"This is where most implementations stumble. WebSockets do not support custom HTTP headers from the browser — it is not possible to send ",[31,443,444],{},"Authorization: Bearer ..."," in the initial handshake via the standard browser WebSocket API.",[15,447,448],{},"Two viable approaches:",[450,451,453],"h3",{"id":452},"approach-1-token-in-the-query-parameter","Approach 1: Token in the Query Parameter",[24,455,457],{"className":26,"code":456,"language":28,"meta":29,"style":29},"from fastapi import WebSocket, WebSocketException, status, Depends, Query\nfrom app.core.security import verify_token\n\nasync def get_websocket_user(\n    websocket: WebSocket,\n    token: str = Query(...),\n) -> str:\n    \"\"\"Extracts and validates the user from the query parameter token.\"\"\"\n    payload = verify_token(token)\n    if payload is None:\n        await websocket.close(code=status.WS_1008_POLICY_VIOLATION)\n        raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)\n    return payload[\"sub\"]\n\n@app.websocket(\"/ws/notifications\")\nasync def notifications_ws(\n    websocket: WebSocket,\n    user_id: str = Depends(get_websocket_user),\n):\n    await manager.connect(websocket, user_id)\n    try:\n        while True:\n            await websocket.receive_text()  # Keep the connection alive\n    except Exception:\n        await manager.disconnect(websocket, user_id)\n",[31,458,459,464,469,473,478,483,488,493,498,503,508,513,518,523,527,532,537,541,546,551,556,561,566,571,576],{"__ignoreMap":29},[34,460,461],{"class":36,"line":37},[34,462,463],{},"from fastapi import WebSocket, WebSocketException, status, Depends, Query\n",[34,465,466],{"class":36,"line":43},[34,467,468],{},"from app.core.security import verify_token\n",[34,470,471],{"class":36,"line":49},[34,472,117],{"emptyLinePlaceholder":116},[34,474,475],{"class":36,"line":55},[34,476,477],{},"async def get_websocket_user(\n",[34,479,480],{"class":36,"line":61},[34,481,482],{},"    websocket: WebSocket,\n",[34,484,485],{"class":36,"line":67},[34,486,487],{},"    token: str = Query(...),\n",[34,489,490],{"class":36,"line":73},[34,491,492],{},") -> str:\n",[34,494,495],{"class":36,"line":129},[34,496,497],{},"    \"\"\"Extracts and validates the user from the query parameter token.\"\"\"\n",[34,499,500],{"class":36,"line":135},[34,501,502],{},"    payload = verify_token(token)\n",[34,504,505],{"class":36,"line":141},[34,506,507],{},"    if payload is None:\n",[34,509,510],{"class":36,"line":147},[34,511,512],{},"        await websocket.close(code=status.WS_1008_POLICY_VIOLATION)\n",[34,514,515],{"class":36,"line":153},[34,516,517],{},"        raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)\n",[34,519,520],{"class":36,"line":159},[34,521,522],{},"    return payload[\"sub\"]\n",[34,524,525],{"class":36,"line":164},[34,526,117],{"emptyLinePlaceholder":116},[34,528,529],{"class":36,"line":170},[34,530,531],{},"@app.websocket(\"/ws/notifications\")\n",[34,533,534],{"class":36,"line":176},[34,535,536],{},"async def notifications_ws(\n",[34,538,539],{"class":36,"line":182},[34,540,482],{},[34,542,543],{"class":36,"line":188},[34,544,545],{},"    user_id: str = Depends(get_websocket_user),\n",[34,547,548],{"class":36,"line":194},[34,549,550],{},"):\n",[34,552,553],{"class":36,"line":200},[34,554,555],{},"    await manager.connect(websocket, user_id)\n",[34,557,558],{"class":36,"line":206},[34,559,560],{},"    try:\n",[34,562,563],{"class":36,"line":211},[34,564,565],{},"        while True:\n",[34,567,568],{"class":36,"line":217},[34,569,570],{},"            await websocket.receive_text()  # Keep the connection alive\n",[34,572,573],{"class":36,"line":222},[34,574,575],{},"    except Exception:\n",[34,577,578],{"class":36,"line":228},[34,579,580],{},"        await manager.disconnect(websocket, user_id)\n",[15,582,583],{},"On the Vue.js client side:",[24,585,589],{"className":586,"code":587,"language":588,"meta":29,"style":29},"language-typescript shiki shiki-themes github-dark github-light","// composables/useWebSocket.ts\nexport function useWebSocket() {\n  const token = useCookie(\"access_token\")\n  const ws = ref\u003CWebSocket | null>(null)\n\n  const connect = () => {\n    ws.value = new WebSocket(\n      `wss://api.myapp.com/ws/notifications?token=${token.value}`,\n    )\n    ws.value.onmessage = (event) => {\n      const message = JSON.parse(event.data)\n      handleMessage(message)\n    }\n    ws.value.onclose = () => {\n      // Automatic reconnection after 3 seconds\n      setTimeout(connect, 3000)\n    }\n  }\n\n  onMounted(connect)\n  onUnmounted(() => ws.value?.close())\n\n  return { ws }\n}\n","typescript",[31,590,591,597,614,639,671,675,693,710,730,735,759,780,788,793,808,813,826,830,835,839,847,866,870,878],{"__ignoreMap":29},[34,592,593],{"class":36,"line":37},[34,594,596],{"class":595},"sryI4","// composables/useWebSocket.ts\n",[34,598,599,603,606,610],{"class":36,"line":43},[34,600,602],{"class":601},"scx8i","export",[34,604,605],{"class":601}," function",[34,607,609],{"class":608},"s-Z4r"," useWebSocket",[34,611,613],{"class":612},"sQ3_J","() {\n",[34,615,616,619,623,626,629,632,636],{"class":36,"line":49},[34,617,618],{"class":601},"  const",[34,620,622],{"class":621},"s0DvM"," token",[34,624,625],{"class":601}," =",[34,627,628],{"class":608}," useCookie",[34,630,631],{"class":612},"(",[34,633,635],{"class":634},"sg6BJ","\"access_token\"",[34,637,638],{"class":612},")\n",[34,640,641,643,646,648,651,654,657,660,663,666,669],{"class":36,"line":55},[34,642,618],{"class":601},[34,644,645],{"class":621}," ws",[34,647,625],{"class":601},[34,649,650],{"class":608}," ref",[34,652,653],{"class":612},"\u003C",[34,655,656],{"class":608},"WebSocket",[34,658,659],{"class":601}," |",[34,661,662],{"class":621}," null",[34,664,665],{"class":612},">(",[34,667,668],{"class":621},"null",[34,670,638],{"class":612},[34,672,673],{"class":36,"line":61},[34,674,117],{"emptyLinePlaceholder":116},[34,676,677,679,682,684,687,690],{"class":36,"line":67},[34,678,618],{"class":601},[34,680,681],{"class":608}," connect",[34,683,625],{"class":601},[34,685,686],{"class":612}," () ",[34,688,689],{"class":601},"=>",[34,691,692],{"class":612}," {\n",[34,694,695,698,701,704,707],{"class":36,"line":73},[34,696,697],{"class":612},"    ws.value ",[34,699,700],{"class":601},"=",[34,702,703],{"class":601}," new",[34,705,706],{"class":608}," WebSocket",[34,708,709],{"class":612},"(\n",[34,711,712,715,718,721,724,727],{"class":36,"line":129},[34,713,714],{"class":634},"      `wss://api.myapp.com/ws/notifications?token=${",[34,716,717],{"class":612},"token",[34,719,720],{"class":634},".",[34,722,723],{"class":612},"value",[34,725,726],{"class":634},"}`",[34,728,729],{"class":612},",\n",[34,731,732],{"class":36,"line":135},[34,733,734],{"class":612},"    )\n",[34,736,737,740,743,745,748,752,755,757],{"class":36,"line":141},[34,738,739],{"class":612},"    ws.value.",[34,741,742],{"class":608},"onmessage",[34,744,625],{"class":601},[34,746,747],{"class":612}," (",[34,749,751],{"class":750},"sFbx2","event",[34,753,754],{"class":612},") ",[34,756,689],{"class":601},[34,758,692],{"class":612},[34,760,761,764,767,769,772,774,777],{"class":36,"line":147},[34,762,763],{"class":601},"      const",[34,765,766],{"class":621}," message",[34,768,625],{"class":601},[34,770,771],{"class":621}," JSON",[34,773,720],{"class":612},[34,775,776],{"class":608},"parse",[34,778,779],{"class":612},"(event.data)\n",[34,781,782,785],{"class":36,"line":153},[34,783,784],{"class":608},"      handleMessage",[34,786,787],{"class":612},"(message)\n",[34,789,790],{"class":36,"line":159},[34,791,792],{"class":612},"    }\n",[34,794,795,797,800,802,804,806],{"class":36,"line":164},[34,796,739],{"class":612},[34,798,799],{"class":608},"onclose",[34,801,625],{"class":601},[34,803,686],{"class":612},[34,805,689],{"class":601},[34,807,692],{"class":612},[34,809,810],{"class":36,"line":170},[34,811,812],{"class":595},"      // Automatic reconnection after 3 seconds\n",[34,814,815,818,821,824],{"class":36,"line":176},[34,816,817],{"class":608},"      setTimeout",[34,819,820],{"class":612},"(connect, ",[34,822,823],{"class":621},"3000",[34,825,638],{"class":612},[34,827,828],{"class":36,"line":182},[34,829,792],{"class":612},[34,831,832],{"class":36,"line":188},[34,833,834],{"class":612},"  }\n",[34,836,837],{"class":36,"line":194},[34,838,117],{"emptyLinePlaceholder":116},[34,840,841,844],{"class":36,"line":200},[34,842,843],{"class":608},"  onMounted",[34,845,846],{"class":612},"(connect)\n",[34,848,849,852,855,857,860,863],{"class":36,"line":206},[34,850,851],{"class":608},"  onUnmounted",[34,853,854],{"class":612},"(() ",[34,856,689],{"class":601},[34,858,859],{"class":612}," ws.value?.",[34,861,862],{"class":608},"close",[34,864,865],{"class":612},"())\n",[34,867,868],{"class":36,"line":211},[34,869,117],{"emptyLinePlaceholder":116},[34,871,872,875],{"class":36,"line":217},[34,873,874],{"class":601},"  return",[34,876,877],{"class":612}," { ws }\n",[34,879,880],{"class":36,"line":222},[34,881,882],{"class":612},"}\n",[450,884,886],{"id":885},"approach-2-session-cookie-more-secure","Approach 2: Session Cookie (More Secure)",[15,888,889],{},"If you are using the BFF pattern with HttpOnly cookies, the WebSocket connection automatically sends the domain cookies — this is the browser's native behaviour:",[24,891,893],{"className":26,"code":892,"language":28,"meta":29,"style":29},"@app.websocket(\"/ws/notifications\")\nasync def notifications_ws(\n    websocket: WebSocket,\n    session: dict = Depends(get_websocket_session),\n):\n    user_id = session[\"user_id\"]\n    await manager.connect(websocket, user_id)\n    # ...\n\nasync def get_websocket_session(websocket: WebSocket) -> dict:\n    session_id = websocket.cookies.get(\"session_id\")\n    if not session_id:\n        await websocket.close(code=status.WS_1008_POLICY_VIOLATION)\n        raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)\n\n    session = await session_manager.get_session_by_id(session_id)\n    if not session:\n        await websocket.close(code=status.WS_1008_POLICY_VIOLATION)\n        raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)\n\n    return session\n",[31,894,895,899,903,907,912,916,921,925,930,934,939,944,949,953,957,961,966,971,975,979,983],{"__ignoreMap":29},[34,896,897],{"class":36,"line":37},[34,898,531],{},[34,900,901],{"class":36,"line":43},[34,902,536],{},[34,904,905],{"class":36,"line":49},[34,906,482],{},[34,908,909],{"class":36,"line":55},[34,910,911],{},"    session: dict = Depends(get_websocket_session),\n",[34,913,914],{"class":36,"line":61},[34,915,550],{},[34,917,918],{"class":36,"line":67},[34,919,920],{},"    user_id = session[\"user_id\"]\n",[34,922,923],{"class":36,"line":73},[34,924,555],{},[34,926,927],{"class":36,"line":129},[34,928,929],{},"    # ...\n",[34,931,932],{"class":36,"line":135},[34,933,117],{"emptyLinePlaceholder":116},[34,935,936],{"class":36,"line":141},[34,937,938],{},"async def get_websocket_session(websocket: WebSocket) -> dict:\n",[34,940,941],{"class":36,"line":147},[34,942,943],{},"    session_id = websocket.cookies.get(\"session_id\")\n",[34,945,946],{"class":36,"line":153},[34,947,948],{},"    if not session_id:\n",[34,950,951],{"class":36,"line":159},[34,952,512],{},[34,954,955],{"class":36,"line":164},[34,956,517],{},[34,958,959],{"class":36,"line":170},[34,960,117],{"emptyLinePlaceholder":116},[34,962,963],{"class":36,"line":176},[34,964,965],{},"    session = await session_manager.get_session_by_id(session_id)\n",[34,967,968],{"class":36,"line":182},[34,969,970],{},"    if not session:\n",[34,972,973],{"class":36,"line":188},[34,974,512],{},[34,976,977],{"class":36,"line":194},[34,978,517],{},[34,980,981],{"class":36,"line":200},[34,982,117],{"emptyLinePlaceholder":116},[34,984,985],{"class":36,"line":206},[34,986,987],{},"    return session\n",[15,989,990],{},"The cookie approach is preferable on a BFF — the token never appears in plaintext in the URL, which would otherwise be visible in server logs.",[19,992,994],{"id":993},"handling-unexpected-disconnections-cleanly","Handling Unexpected Disconnections Cleanly",[15,996,997],{},"A WebSocket connection can die in several ways: the user closes the tab, the network drops, the client pod restarts. These cases must be detected and cleaned up:",[24,999,1001],{"className":26,"code":1000,"language":28,"meta":29,"style":29},"@app.websocket(\"/ws/notifications\")\nasync def notifications_ws(\n    websocket: WebSocket,\n    user_id: str = Depends(get_websocket_user),\n):\n    await manager.connect(websocket, user_id)\n    try:\n        while True:\n            try:\n                # Timeout on receive — detects dead connections\n                data = await asyncio.wait_for(\n                    websocket.receive_text(),\n                    timeout=30.0\n                )\n                await handle_client_message(user_id, data)\n\n            except asyncio.TimeoutError:\n                # Send a ping to verify the client is still alive\n                try:\n                    await websocket.send_json({\"type\": \"ping\"})\n                except Exception:\n                    break  # Dead connection — exit the loop\n\n    except Exception as e:\n        logger.info(f\"WebSocket closed for user={user_id}: {type(e).__name__}\")\n    finally:\n        # Cleanup guaranteed regardless of how the connection ended\n        await manager.disconnect(websocket, user_id)\n",[31,1002,1003,1007,1011,1015,1019,1023,1027,1031,1035,1039,1044,1049,1054,1059,1064,1069,1073,1078,1083,1088,1093,1098,1103,1107,1112,1117,1122,1127],{"__ignoreMap":29},[34,1004,1005],{"class":36,"line":37},[34,1006,531],{},[34,1008,1009],{"class":36,"line":43},[34,1010,536],{},[34,1012,1013],{"class":36,"line":49},[34,1014,482],{},[34,1016,1017],{"class":36,"line":55},[34,1018,545],{},[34,1020,1021],{"class":36,"line":61},[34,1022,550],{},[34,1024,1025],{"class":36,"line":67},[34,1026,555],{},[34,1028,1029],{"class":36,"line":73},[34,1030,560],{},[34,1032,1033],{"class":36,"line":129},[34,1034,565],{},[34,1036,1037],{"class":36,"line":135},[34,1038,307],{},[34,1040,1041],{"class":36,"line":141},[34,1042,1043],{},"                # Timeout on receive — detects dead connections\n",[34,1045,1046],{"class":36,"line":147},[34,1047,1048],{},"                data = await asyncio.wait_for(\n",[34,1050,1051],{"class":36,"line":153},[34,1052,1053],{},"                    websocket.receive_text(),\n",[34,1055,1056],{"class":36,"line":159},[34,1057,1058],{},"                    timeout=30.0\n",[34,1060,1061],{"class":36,"line":164},[34,1062,1063],{},"                )\n",[34,1065,1066],{"class":36,"line":170},[34,1067,1068],{},"                await handle_client_message(user_id, data)\n",[34,1070,1071],{"class":36,"line":176},[34,1072,117],{"emptyLinePlaceholder":116},[34,1074,1075],{"class":36,"line":182},[34,1076,1077],{},"            except asyncio.TimeoutError:\n",[34,1079,1080],{"class":36,"line":188},[34,1081,1082],{},"                # Send a ping to verify the client is still alive\n",[34,1084,1085],{"class":36,"line":194},[34,1086,1087],{},"                try:\n",[34,1089,1090],{"class":36,"line":200},[34,1091,1092],{},"                    await websocket.send_json({\"type\": \"ping\"})\n",[34,1094,1095],{"class":36,"line":206},[34,1096,1097],{},"                except Exception:\n",[34,1099,1100],{"class":36,"line":211},[34,1101,1102],{},"                    break  # Dead connection — exit the loop\n",[34,1104,1105],{"class":36,"line":217},[34,1106,117],{"emptyLinePlaceholder":116},[34,1108,1109],{"class":36,"line":222},[34,1110,1111],{},"    except Exception as e:\n",[34,1113,1114],{"class":36,"line":228},[34,1115,1116],{},"        logger.info(f\"WebSocket closed for user={user_id}: {type(e).__name__}\")\n",[34,1118,1119],{"class":36,"line":234},[34,1120,1121],{},"    finally:\n",[34,1123,1124],{"class":36,"line":240},[34,1125,1126],{},"        # Cleanup guaranteed regardless of how the connection ended\n",[34,1128,1129],{"class":36,"line":246},[34,1130,580],{},[15,1132,426,1133,1136,1137,1140],{},[31,1134,1135],{},"try/finally"," pattern around the main loop guarantees that ",[31,1138,1139],{},"disconnect"," is always called, regardless of the reason for closure.",[19,1142,1144],{"id":1143},"broadcasting-events-from-anywhere-in-the-application","Broadcasting Events From Anywhere in the Application",[15,1146,1147],{},"The real use case: a backend process completes and needs to notify connected clients in real time.",[24,1149,1151],{"className":26,"code":1150,"language":28,"meta":29,"style":29},"class CertificateService:\n    def __init__(self, repo: CertificateRepository, ws_manager: ConnectionManager):\n        self.repo = repo\n        self.ws_manager = ws_manager\n\n    async def process_certificate(self, cert_id: str, user_id: str) -> Certificate:\n        certificate = await self.repo.process(cert_id)\n\n        # Notify the user in real time\n        await self.ws_manager.send_to_user(user_id, {\n            \"type\": \"certificate_processed\",\n            \"data\": {\n                \"id\": certificate.id,\n                \"status\": certificate.status,\n                \"volume\": certificate.volume,\n            }\n        })\n\n        return certificate\n",[31,1152,1153,1158,1163,1168,1173,1177,1182,1187,1191,1196,1201,1206,1211,1216,1221,1226,1231,1236,1240],{"__ignoreMap":29},[34,1154,1155],{"class":36,"line":37},[34,1156,1157],{},"class CertificateService:\n",[34,1159,1160],{"class":36,"line":43},[34,1161,1162],{},"    def __init__(self, repo: CertificateRepository, ws_manager: ConnectionManager):\n",[34,1164,1165],{"class":36,"line":49},[34,1166,1167],{},"        self.repo = repo\n",[34,1169,1170],{"class":36,"line":55},[34,1171,1172],{},"        self.ws_manager = ws_manager\n",[34,1174,1175],{"class":36,"line":61},[34,1176,117],{"emptyLinePlaceholder":116},[34,1178,1179],{"class":36,"line":67},[34,1180,1181],{},"    async def process_certificate(self, cert_id: str, user_id: str) -> Certificate:\n",[34,1183,1184],{"class":36,"line":73},[34,1185,1186],{},"        certificate = await self.repo.process(cert_id)\n",[34,1188,1189],{"class":36,"line":129},[34,1190,117],{"emptyLinePlaceholder":116},[34,1192,1193],{"class":36,"line":135},[34,1194,1195],{},"        # Notify the user in real time\n",[34,1197,1198],{"class":36,"line":141},[34,1199,1200],{},"        await self.ws_manager.send_to_user(user_id, {\n",[34,1202,1203],{"class":36,"line":147},[34,1204,1205],{},"            \"type\": \"certificate_processed\",\n",[34,1207,1208],{"class":36,"line":153},[34,1209,1210],{},"            \"data\": {\n",[34,1212,1213],{"class":36,"line":159},[34,1214,1215],{},"                \"id\": certificate.id,\n",[34,1217,1218],{"class":36,"line":164},[34,1219,1220],{},"                \"status\": certificate.status,\n",[34,1222,1223],{"class":36,"line":170},[34,1224,1225],{},"                \"volume\": certificate.volume,\n",[34,1227,1228],{"class":36,"line":176},[34,1229,1230],{},"            }\n",[34,1232,1233],{"class":36,"line":182},[34,1234,1235],{},"        })\n",[34,1237,1238],{"class":36,"line":188},[34,1239,117],{"emptyLinePlaceholder":116},[34,1241,1242],{"class":36,"line":194},[34,1243,1244],{},"        return certificate\n",[15,1246,426,1247,1250],{},[31,1248,1249],{},"ConnectionManager"," is injected as a FastAPI dependency — a singleton shared across the entire process:",[24,1252,1254],{"className":26,"code":1253,"language":28,"meta":29,"style":29},"# app/api/dependencies.py\nfrom app.api.websockets import manager\n\ndef get_ws_manager() -> ConnectionManager:\n    return manager\n",[31,1255,1256,1261,1266,1270,1275],{"__ignoreMap":29},[34,1257,1258],{"class":36,"line":37},[34,1259,1260],{},"# app/api/dependencies.py\n",[34,1262,1263],{"class":36,"line":43},[34,1264,1265],{},"from app.api.websockets import manager\n",[34,1267,1268],{"class":36,"line":49},[34,1269,117],{"emptyLinePlaceholder":116},[34,1271,1272],{"class":36,"line":55},[34,1273,1274],{},"def get_ws_manager() -> ConnectionManager:\n",[34,1276,1277],{"class":36,"line":61},[34,1278,1279],{},"    return manager\n",[19,1281,1283],{"id":1282},"the-multi-pod-problem-distributed-state-with-redis-pubsub","The Multi-Pod Problem: Distributed State with Redis Pub/Sub",[15,1285,426,1286,1288],{},[31,1287,1249],{}," as described above has a critical limitation: it is in-memory. On a multi-pod OpenShift deployment with three replicas, each pod has its own manager. An event processed on pod A will not be broadcast to clients connected to pod B or C.",[15,1290,1291],{},"The solution: Redis Pub/Sub as an inter-pod event bus.",[24,1293,1295],{"className":26,"code":1294,"language":28,"meta":29,"style":29},"import redis.asyncio as aioredis\nimport json\nimport asyncio\n\nclass DistributedConnectionManager(ConnectionManager):\n    def __init__(self, redis: aioredis.Redis):\n        super().__init__()\n        self.redis = redis\n        self.channel = \"ws:broadcast\"\n\n    async def publish_to_user(self, user_id: str, message: dict) -> None:\n        \"\"\"Publishes an event to Redis — all pods receive it.\"\"\"\n        await self.redis.publish(\n            f\"ws:user:{user_id}\",\n            json.dumps(message)\n        )\n\n    async def publish_broadcast(self, message: dict) -> None:\n        \"\"\"Publishes a broadcast to Redis.\"\"\"\n        await self.redis.publish(self.channel, json.dumps(message))\n\n    async def start_subscriber(self) -> None:\n        \"\"\"To be started at pod startup — listens for Redis events.\"\"\"\n        pubsub = self.redis.pubsub()\n        await pubsub.psubscribe(\"ws:user:*\", self.channel)\n\n        async for message in pubsub.listen():\n            if message[\"type\"] != \"pmessage\" and message[\"type\"] != \"message\":\n                continue\n\n            channel = message[\"channel\"].decode()\n            data = json.loads(message[\"data\"])\n\n            if channel == self.channel:\n                # Local broadcast to clients on THIS pod\n                await super().broadcast(data)\n            elif channel.startswith(\"ws:user:\"):\n                user_id = channel.split(\":\")[-1]\n                # Send to clients on THIS pod for this user\n                await super().send_to_user(user_id, data)\n",[31,1296,1297,1302,1307,1311,1315,1320,1325,1330,1335,1340,1344,1349,1354,1359,1364,1369,1374,1378,1383,1388,1393,1397,1402,1407,1412,1417,1421,1426,1431,1436,1440,1445,1450,1454,1459,1464,1469,1474,1479,1484],{"__ignoreMap":29},[34,1298,1299],{"class":36,"line":37},[34,1300,1301],{},"import redis.asyncio as aioredis\n",[34,1303,1304],{"class":36,"line":43},[34,1305,1306],{},"import json\n",[34,1308,1309],{"class":36,"line":49},[34,1310,96],{},[34,1312,1313],{"class":36,"line":55},[34,1314,117],{"emptyLinePlaceholder":116},[34,1316,1317],{"class":36,"line":61},[34,1318,1319],{},"class DistributedConnectionManager(ConnectionManager):\n",[34,1321,1322],{"class":36,"line":67},[34,1323,1324],{},"    def __init__(self, redis: aioredis.Redis):\n",[34,1326,1327],{"class":36,"line":73},[34,1328,1329],{},"        super().__init__()\n",[34,1331,1332],{"class":36,"line":129},[34,1333,1334],{},"        self.redis = redis\n",[34,1336,1337],{"class":36,"line":135},[34,1338,1339],{},"        self.channel = \"ws:broadcast\"\n",[34,1341,1342],{"class":36,"line":141},[34,1343,117],{"emptyLinePlaceholder":116},[34,1345,1346],{"class":36,"line":147},[34,1347,1348],{},"    async def publish_to_user(self, user_id: str, message: dict) -> None:\n",[34,1350,1351],{"class":36,"line":153},[34,1352,1353],{},"        \"\"\"Publishes an event to Redis — all pods receive it.\"\"\"\n",[34,1355,1356],{"class":36,"line":159},[34,1357,1358],{},"        await self.redis.publish(\n",[34,1360,1361],{"class":36,"line":164},[34,1362,1363],{},"            f\"ws:user:{user_id}\",\n",[34,1365,1366],{"class":36,"line":170},[34,1367,1368],{},"            json.dumps(message)\n",[34,1370,1371],{"class":36,"line":176},[34,1372,1373],{},"        )\n",[34,1375,1376],{"class":36,"line":182},[34,1377,117],{"emptyLinePlaceholder":116},[34,1379,1380],{"class":36,"line":188},[34,1381,1382],{},"    async def publish_broadcast(self, message: dict) -> None:\n",[34,1384,1385],{"class":36,"line":194},[34,1386,1387],{},"        \"\"\"Publishes a broadcast to Redis.\"\"\"\n",[34,1389,1390],{"class":36,"line":200},[34,1391,1392],{},"        await self.redis.publish(self.channel, json.dumps(message))\n",[34,1394,1395],{"class":36,"line":206},[34,1396,117],{"emptyLinePlaceholder":116},[34,1398,1399],{"class":36,"line":211},[34,1400,1401],{},"    async def start_subscriber(self) -> None:\n",[34,1403,1404],{"class":36,"line":217},[34,1405,1406],{},"        \"\"\"To be started at pod startup — listens for Redis events.\"\"\"\n",[34,1408,1409],{"class":36,"line":222},[34,1410,1411],{},"        pubsub = self.redis.pubsub()\n",[34,1413,1414],{"class":36,"line":228},[34,1415,1416],{},"        await pubsub.psubscribe(\"ws:user:*\", self.channel)\n",[34,1418,1419],{"class":36,"line":234},[34,1420,117],{"emptyLinePlaceholder":116},[34,1422,1423],{"class":36,"line":240},[34,1424,1425],{},"        async for message in pubsub.listen():\n",[34,1427,1428],{"class":36,"line":246},[34,1429,1430],{},"            if message[\"type\"] != \"pmessage\" and message[\"type\"] != \"message\":\n",[34,1432,1433],{"class":36,"line":252},[34,1434,1435],{},"                continue\n",[34,1437,1438],{"class":36,"line":258},[34,1439,117],{"emptyLinePlaceholder":116},[34,1441,1442],{"class":36,"line":264},[34,1443,1444],{},"            channel = message[\"channel\"].decode()\n",[34,1446,1447],{"class":36,"line":269},[34,1448,1449],{},"            data = json.loads(message[\"data\"])\n",[34,1451,1452],{"class":36,"line":275},[34,1453,117],{"emptyLinePlaceholder":116},[34,1455,1456],{"class":36,"line":281},[34,1457,1458],{},"            if channel == self.channel:\n",[34,1460,1461],{"class":36,"line":287},[34,1462,1463],{},"                # Local broadcast to clients on THIS pod\n",[34,1465,1466],{"class":36,"line":293},[34,1467,1468],{},"                await super().broadcast(data)\n",[34,1470,1471],{"class":36,"line":298},[34,1472,1473],{},"            elif channel.startswith(\"ws:user:\"):\n",[34,1475,1476],{"class":36,"line":304},[34,1477,1478],{},"                user_id = channel.split(\":\")[-1]\n",[34,1480,1481],{"class":36,"line":310},[34,1482,1483],{},"                # Send to clients on THIS pod for this user\n",[34,1485,1486],{"class":36,"line":316},[34,1487,1488],{},"                await super().send_to_user(user_id, data)\n",[15,1490,1491,1492,1495],{},"Starting the subscriber in the FastAPI ",[31,1493,1494],{},"lifespan",":",[24,1497,1499],{"className":26,"code":1498,"language":28,"meta":29,"style":29},"@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    task = asyncio.create_task(distributed_manager.start_subscriber())\n    background_tasks.add(task)\n    task.add_done_callback(background_tasks.discard)\n\n    yield\n\n    task.cancel()\n    await asyncio.gather(task, return_exceptions=True)\n",[31,1500,1501,1506,1511,1516,1521,1526,1530,1535,1539,1544],{"__ignoreMap":29},[34,1502,1503],{"class":36,"line":37},[34,1504,1505],{},"@asynccontextmanager\n",[34,1507,1508],{"class":36,"line":43},[34,1509,1510],{},"async def lifespan(app: FastAPI):\n",[34,1512,1513],{"class":36,"line":49},[34,1514,1515],{},"    task = asyncio.create_task(distributed_manager.start_subscriber())\n",[34,1517,1518],{"class":36,"line":55},[34,1519,1520],{},"    background_tasks.add(task)\n",[34,1522,1523],{"class":36,"line":61},[34,1524,1525],{},"    task.add_done_callback(background_tasks.discard)\n",[34,1527,1528],{"class":36,"line":67},[34,1529,117],{"emptyLinePlaceholder":116},[34,1531,1532],{"class":36,"line":73},[34,1533,1534],{},"    yield\n",[34,1536,1537],{"class":36,"line":129},[34,1538,117],{"emptyLinePlaceholder":116},[34,1540,1541],{"class":36,"line":135},[34,1542,1543],{},"    task.cancel()\n",[34,1545,1546],{"class":36,"line":141},[34,1547,1548],{},"    await asyncio.gather(task, return_exceptions=True)\n",[15,1550,1551,1552,1555],{},"With this architecture, a service on pod A calls ",[31,1553,1554],{},"publish_to_user()"," — Redis propagates the event to all pods, and each pod delivers it locally to the relevant clients.",[19,1557,1559],{"id":1558},"key-takeaways","Key Takeaways",[15,1561,1562,1563,1565,1566,1569,1570,1573],{},"Production-grade WebSockets require solving four distinct problems: authentication (cookie or query parameter depending on the architecture), dead connection handling (ping/timeout with ",[31,1564,1135],{},"), broadcasting to multiple connections per user (",[31,1567,1568],{},"list[WebSocket]"," per ",[31,1571,1572],{},"user_id","), and multi-pod distribution (Redis Pub/Sub). Each problem is straightforward in isolation — it is their combination that determines whether a WebSocket implementation holds up under real-world conditions.",[1575,1576,1577],"style",{},"html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sryI4, html code.shiki .sryI4{--shiki-dark:#6A737D;--shiki-default:#6A737D}html pre.shiki code .scx8i, html code.shiki .scx8i{--shiki-dark:#F97583;--shiki-default:#D73A49}html pre.shiki code .s-Z4r, html code.shiki .s-Z4r{--shiki-dark:#B392F0;--shiki-default:#6F42C1}html pre.shiki code .sQ3_J, html code.shiki .sQ3_J{--shiki-dark:#E1E4E8;--shiki-default:#24292E}html pre.shiki code .s0DvM, html code.shiki .s0DvM{--shiki-dark:#79B8FF;--shiki-default:#005CC5}html pre.shiki code .sg6BJ, html code.shiki .sg6BJ{--shiki-dark:#9ECBFF;--shiki-default:#032F62}html pre.shiki code .sFbx2, html code.shiki .sFbx2{--shiki-dark:#FFAB70;--shiki-default:#E36209}",{"title":29,"searchDepth":43,"depth":43,"links":1579},[1580,1581,1582,1586,1587,1588,1589],{"id":21,"depth":43,"text":22},{"id":82,"depth":43,"text":83},{"id":437,"depth":43,"text":438,"children":1583},[1584,1585],{"id":452,"depth":49,"text":453},{"id":885,"depth":49,"text":886},{"id":993,"depth":43,"text":994},{"id":1143,"depth":43,"text":1144},{"id":1282,"depth":43,"text":1283},{"id":1558,"depth":43,"text":1559},"2025-03-13",null,"md",{},"/en/blog/websockets-fastapi",{"title":5,"description":17},"websockets-fastapi","en/blog/websockets-fastapi",[1599,1600,1601,1602],"FastAPI","WebSockets","Python","Real-time","9P-B3idvAfGVELqd9UDMeKY_PpsqrJBfZCceq7bvBvg",1774645635810]