FastAPI, WebSocket, asyncio: why my websocket connection gets blocked

1 week ago 3
ARTICLE AD BOX

I have a simple FastAPI server with just one WebSocket endpoint. It receives a terminal command, executes it locally, and broadcasts the stdout to the client every second:

from fastapi import FastAPI, WebSocket, Body from starlette.websockets import WebSocketDisconnect from back.SyncLJM import LocalProcessManager as LPM import asyncio import time app = FastAPI() @app.websocket("/cmd") async def ws_endpoint(websocket: WebSocket): await websocket.accept() try: while True: cmd_str = await websocket.receive_text() await websocket.send_text(f"Received command '{cmd_str}'") proc_mgr = LPM() pid = proc_mgr.submit(cmd_str) await websocket.send_text(f"process stated with pid {pid}") while proc_mgr.status() is False: # await asyncio.sleep(1) time.sleep(1) stdout = proc_mgr.read_stdout() if stdout is not None and stdout != '': print(f"===> {stdout}") await websocket.send_text(stdout) stdout = proc_mgr.read_stdout() if stdout is not None and stdout != '': await websocket.send_text(stdout) await websocket.send_text("=== PROCESS FINISHED ===") except WebSocketDisconnect as e: print(f"Connection closed {e.code}")

LocalProcessManager is my own process management class, built with Popen and threading.

I also have a simple client script that sends a terminal command to the server and listens for the command's stdout:

async def run_cmd(): async with connect("ws://127.0.0.1:8000/cmd") as websocket: await websocket.send("ping -n 10 localhost") while True: print('!!!') stdout = await websocket.recv() print(stdout.strip()) if "=== PROCESS FINISHED ===" in stdout: print(f"finishing client") break asyncio.run(run_cmd())

The issue is this: when I use the synchronous time.sleep(1) in my server loop, my client receives the entire stdout only after the command completes. However, when I use the asynchronous await asyncio.sleep(1), I get the expected behavior: the client receives chunks of stdout every second.

I don't understand why this happens. My synchronous time.sleep(1) blocks the event loop for only one second, but the command execution takes about 10 seconds in total...

Read Entire Article