FastAPI WebSocket: Endpoints, Broadcasting and Testing

In FastAPI you declare a socket route with @app.websocket("/ws") and an async function that takes a WebSocket. Call await websocket.accept(), then loop over receive_text() and send_text(). Catch WebSocketDisconnect to clean up when a client leaves. Install the websockets package first, and test with TestClient.websocket_connect().

The WebSocket methods you will call most
CallWhat it does
await websocket.accept()Completes the handshake; nothing can be sent before it
await websocket.receive_text()Waits for the next text message
await websocket.receive_json()Waits for a message and parses it as JSON
await websocket.send_text(s)Sends a text message
await websocket.send_json(obj)Serialises an object and sends it
await websocket.close(code=1000)Ends the connection with a close code

Updated

The endpoint builder below writes a working main.py from a few choices: the route path, whether messages are plain text or JSON, whether every client hears every message, and whether a token must arrive in the query string. A matching pytest file comes with it. Every option maps to a pattern from the FastAPI documentation, rewritten here as a single runnable file.

FastAPI WebSocket endpoint builder

Choose how the endpoint should behave and copy a complete main.py plus a pytest file that checks it.

The smallest working endpoint

main.py: echo

Accept, then echo each message until the client disconnects.

from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

@app.websocket("/ws")
async def echo(websocket: WebSocket):
    await websocket.accept()
    try:
        while True:
            text = await websocket.receive_text()
            await websocket.send_text(f"echo: {text}")
    except WebSocketDisconnect:
        print("client left")

Broadcast with a connection manager

A manager class shared by every connection

Works for a single process. Swap the list for a Redis-backed relay before you scale out.

class ConnectionManager:
    def __init__(self) -> None:
        self.active: list[WebSocket] = []

    async def connect(self, websocket: WebSocket) -> None:
        await websocket.accept()
        self.active.append(websocket)

    def disconnect(self, websocket: WebSocket) -> None:
        if websocket in self.active:
            self.active.remove(websocket)

    async def broadcast(self, message: str) -> None:
        for connection in list(self.active):
            await connection.send_text(message)

manager = ConnectionManager()

@app.websocket("/chat")
async def chat(websocket: WebSocket):
    await manager.connect(websocket)
    try:
        while True:
            await manager.broadcast(await websocket.receive_text())
    except WebSocketDisconnect:
        manager.disconnect(websocket)

Testing with TestClient

test_main.py

No server needed: the client talks to the app in process. Run it with pytest.

from fastapi.testclient import TestClient
from main import app

def test_echo():
    client = TestClient(app)
    with client.websocket_connect("/ws") as ws:
        ws.send_text("hi")
        assert ws.receive_text() == "echo: hi"

What the endpoint loop is doing

A socket handler is a long-running coroutine. It accepts once, then sits on await receive_text() until the next message, so a single client costs almost nothing while idle. When the client goes away, the pending receive raises WebSocketDisconnect, and that is your moment to tidy up.

Because the handler is async, other clients keep being served while one is waiting. Avoid blocking calls such as time.sleep or synchronous database drivers inside the loop; they freeze every connection in the worker.

Authentication before accept

Read the token from the query string as a normal parameter. You then have two ways to refuse it. Closing before accept() turns into an HTTP 403 on the handshake, which a browser only reports as a failed connection. Accepting and then closing with code 1008 lets the client read the reason and show a sign-in prompt instead of retrying forever.

How it works

  1. Pick the options in the builder and copy main.py into an empty folder.
  2. Install the pieces: pip install "fastapi[standard]" websockets, then start the app with fastapi dev main.py.
  3. Connect from the We.Inc WebSocket tester or a browser console at ws://localhost:8000 plus your path.
  4. Copy the generated test file and run pytest to check the endpoint without starting a server.

Tips

Frequently asked questions

Why does my FastAPI WebSocket return 404 or fail to upgrade?

Start with the install: the FastAPI WebSocket docs ask you to add the websockets package alongside FastAPI. Then check the path, because a route declared with @app.websocket only answers socket upgrades, never plain HTTP GETs. Closing the socket before accept() also shows up as a refused handshake (HTTP 403).

How do I broadcast a message to every client?

Keep accepted sockets in a list inside a small manager class, add on connect, remove on disconnect, and loop over the list calling send_text. The builder above generates exactly this when you tick the broadcast option.

Does broadcasting work with several Uvicorn workers?

Not with an in-memory list. Each worker is a separate process with its own list. The FastAPI docs point to encode/broadcaster, which relays messages through Redis or PostgreSQL so every process hears every message.

Can I use Depends and Query on a WebSocket route?

Yes. WebSocket endpoints accept Depends, Query, Cookie, Header, Path and Security just like HTTP routes, so an authentication dependency can be shared between both.

How do I test a WebSocket endpoint?

Create a TestClient for the app and open the socket in a with block using client.websocket_connect("/ws"). Inside it, send_text and receive_text drive the conversation, and leaving the block closes the connection.

Related: WebSocket tester, WebSocket in Node.js, CORS explained, nginx load balancing, Webhook vs API.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap