Tornado (Python)
react-websocket speaks plain WebSocket, so your server can be written in any language. This recipe pairs it with Tornado, a Python web framework. The result is a small broadcast chat: every message goes to everyone who's connected.
It follows the repository's Tornado example. The server runs with uv, which sets up Python and Tornado by itself. Start it in two terminals:
Then open http://localhost:5173 in two tabs and send a message from each.
The short version
- A
WebSocketHandlerwithopen,on_closeandon_messageis the whole server. - Vite serves the page from another port, so in development the server has to accept that origin.
- Turn on
websocket_ping_interval, so connections that died without closing get cleaned up. - On the client, it's an ordinary
createWebSocketwithjson: true. Nothing Python-specific.
The server
The handler keeps every open socket in a set, and sends each message to all of them:
Here's what each part does:
openandon_closeadd and remove the socket.on_messageloops over the set and passes the message on as it came: text as text, binary as binary.check_origindecides which pages may connect. Tornado's default only allows a page from the same host and port. Vite serves the page on 5173 and Tornado listens on 8888, so the example allows anylocalhostpage.websocket_ping_interval=10pings every client every 10 seconds and closes the ones that don't answer. Without it, a laptop that went to sleep would stay inclientsfor a long time. Browsers answer these pings by themselves, so the client needs no code for them.websocket_max_message_sizecaps each message at 64 KiB. Tornado's default is 10 MiB. A bigger message closes the connection with code1009, and the client reconnects.
Run it with uv
The server's dependencies are written at the top of server.py, as inline script metadata:
So uv run creates the environment, installs Tornado and even downloads Python 3.11+ if you don't have it. There's no virtualenv to create and no pip install to remember:
The exact versions are pinned in server.py.lock, next to the script. To move to the newest Tornado that still matches tornado>=6.5,<7:
Prefer pip? That works too:
The client
The client is a normal Vite + React app. Create one shared connection and use it from your components:
use(live.ready) shows "Waiting for the server…" until the first connection opens. Later reconnects don't bring the placeholder back, so the messages stay on screen.
The example itself imports from ../../src instead of react-websocket, so it runs the repository's working copy without a build. In your app, import from react-websocket as above.
With json: true, send() turns your object into JSON text, and the server passes that text to everyone unchanged. Each client turns it back into an object before your listeners see it.
Use another port
The server takes a --port flag, and the client reads its URL from VITE_WS_URL:
Let the browser notice a dead server
The server's pings find dead clients. The opposite case, a browser whose server has quietly gone away, needs a heartbeat on the client. Browsers can't send WebSocket ping frames from JavaScript, so the heartbeat is an ordinary message that the server answers.
Tell the client what to send and what the answer looks like:
And answer it at the top of on_message, before the broadcast:
The return matters. Without it, the server would broadcast ping to everyone, and every client would report it as a message that isn't valid JSON. Setting pong on the client means the answer is swallowed, so your components never see it.
Going to production
-
Serve the page from the same origin as the WebSocket, and delete
check_origin. Tornado's default check then allows your own page and nothing else. -
Or check against your own domains if the page lives elsewhere:
-
Point the client at the real server with
VITE_WS_URLwhen you build, for exampleVITE_WS_URL=wss://example.com/live. Usewss://on an HTTPS page.
Try it
With both parts running:
- Open two tabs and send a message from each. Both tabs show both messages.
- Stop the server with Ctrl+C. The status goes to
reconnecting. - Send a few messages while it's down. They're queued. Start the server again, and they go out to every tab that's connected by then. The server keeps no history, so a tab that connects later won't see them.
- Edit
server.py. Tornado restarts itself, and the page reconnects on its own.
See also
- Reconnecting and heartbeats: backoff, heartbeats and the send queue
- Messages and types: JSON, binary data and schemas
- Authentication: sending a token when browsers can't set headers
- Hono: the same kind of chat, with a TypeScript server and shared types