WebSocketsWebSockets

WebSockets

After mastering Server-Sent Events for one-way communication, Batman realized he needed something more powerful. When Commissioner Gordon wanted to chat with him in real-time during crisis situations, Batman needed bidirectional communication.

"SSE is great for pushing updates to my dashboard," Batman thought, "but I need two-way communication for coordinating with my allies!"

To handle real-time bidirectional communication, Batman learned how to work with WebSockets using Robyn's modern decorator-based API. Under the hood, messages flow through Rust channels for maximum performance — no Python GIL overhead during message dispatch.

Request

WebSocket
/web_socket
from robyn import Robyn

app = Robyn(__file__)

@app.websocket("/web_socket")
async def handler(websocket):
    while True:
        msg = await websocket.receive_text()
        await websocket.send_text(f"Echo: {msg}")

app.start()

Receiving Messages

Robyn provides several methods for receiving WebSocket messages, all backed by a Rust tokio::mpsc channel so the Python handler genuinely suspends without holding the GIL.

  • receive() returns the next frame as str (text frame) or bytes (binary frame).
  • receive_text() returns the next text frame, raising TypeError if a binary frame arrives.
  • receive_bytes() returns the next binary frame, raising TypeError if a text frame arrives.
  • receive_json() receives a text frame and JSON-decodes it.

All receive methods raise WebSocketDisconnect when the client disconnects. You can either catch it explicitly or let the internal wrapper handle it silently.

Receiving Messages

WebSocket
/web_socket
@app.websocket("/ws")
async def handler(websocket):
    try:
        while True:
            msg = await websocket.receive_text()
            await websocket.send_text(f"Got: {msg}")
    except WebSocketDisconnect:
        print(f"Client {websocket.id} disconnected")

Sending Messages

To send a message to the current client, use any of the send methods. All are async.

  • send(data) accepts str or bytes — a str sends a text frame, bytes sends a binary frame.
  • send_text(data) sends a text frame.
  • send_bytes(data) sends a binary frame.
  • send_json(data) serializes to JSON and sends a text frame.

Sending Messages

WebSocket
/web_socket
@app.websocket("/ws")
async def handler(websocket):
    while True:
        msg = await websocket.receive_text()
        await websocket.send_text(f"Echo: {msg}")

Broadcasting

To send a message to all connected clients on the same WebSocket endpoint, use the broadcast() method. It accepts both str (text frame) and bytes (binary frame).

Broadcasting

WebSocket
/chat
@app.websocket("/chat")
async def handler(websocket):
    while True:
        msg = await websocket.receive_text()
        # Send to all connected clients
        await websocket.broadcast(f"User {websocket.id}: {msg}")
        # Also send a confirmation to this client only
        await websocket.send_text("Your message was sent")

Query Parameters

You can access query parameters from the WebSocket connection URL via websocket.query_params.

Query Params

WebSocket
/ws?name=gordon&role=commissioner
@app.websocket("/ws")
async def handler(websocket):
    name = websocket.query_params.get("name")
    role = websocket.query_params.get("role")

    if name == "gordon" and role == "commissioner":
        await websocket.broadcast("Gordon authorized!")

    while True:
        msg = await websocket.receive_text()
        await websocket.send_text(f"Hello {name}: {msg}")

Easy Access Query Parameters

Instead of manually calling websocket.query_params.get(...), you can declare typed query parameters directly in your handler, on_connect, and on_close signatures. Robyn will automatically resolve and coerce them — just like HTTP easy access parameters.

Parameters with defaults are optional. Parameters without defaults are required — if missing, the connection is rejected with an error message.

Easy Access Query Params

WebSocket
/ws?room=chat&page=5
@app.websocket("/ws")
async def handler(websocket, room: str = "default", page: int = 1):
    try:
        while True:
            msg = await websocket.receive_text()
            await websocket.send_text(
                f"room={room} page={page} msg={msg}"
            )
    except WebSocketDisconnect:
        pass

Closing Connections

To programmatically close a WebSocket connection from the server side, use websocket.close(). This will:

  1. Close the WebSocket connection.
  2. Remove the client from the WebSocket registry.
  3. Cause any pending receive() / receive_text() / receive_bytes() to raise WebSocketDisconnect.

Close Connection

WebSocket
/ws
@app.websocket("/ws")
async def handler(websocket):
    while True:
        msg = await websocket.receive_text()
        if msg == "quit":
            await websocket.close()
            break
        await websocket.send_text(f"Got: {msg}")

Connect and Close Callbacks

You can attach optional on_connect and on_close callbacks to your WebSocket handler. These are decorators on the handler function itself.

  • on_connect is called when a new client connects. Its return value is sent to the client as the first message.
  • on_close is called when the connection closes. Its return value is sent to the client as the final message.

Both callbacks receive a websocket object with access to id and query_params. Both are optional.

Callbacks

WebSocket
/chat
@app.websocket("/chat")
async def chat(websocket):
    while True:
        msg = await websocket.receive_text()
        await websocket.broadcast(msg)

@chat.on_connect
def on_connect(websocket):
    return f"Welcome, {websocket.id}!"

@chat.on_close
def on_close(websocket):
    return "Goodbye!"

WebSocket API Reference

The websocket object passed to handlers exposes the following methods and properties:

Method / PropertyDescription

| await websocket.receive() | Block until next frame; returns str for text frames, bytes for binary frames; raises WebSocketDisconnect on close | | await websocket.receive_text() | Block until next text frame; raises TypeError if a binary frame arrives; raises WebSocketDisconnect on close | | await websocket.receive_bytes() | Block until next binary frame; raises TypeError if a text frame arrives; raises WebSocketDisconnect on close | | await websocket.receive_json() | Receive a text frame and JSON-decode it | | await websocket.send(data) | Send str as a text frame or bytes as a binary frame | | await websocket.send_text(data) | Send a text frame to this client | | await websocket.send_bytes(data) | Send a binary frame to this client | | await websocket.send_json(data) | Send JSON as a text frame to this client | | await websocket.broadcast(data) | Broadcast str (text) or bytes (binary) to all clients on this endpoint | | await websocket.close() | Close the connection server-side | | websocket.id | Connection UUID string | | websocket.query_params | Query parameters from the connection URL |

What's next?

As the codebase grew, Batman wanted to onboard the justice league to help him manage the application.

Robyn told him about the different ways he could scale his application, and how to use views and subrouters to make his code more readable.