Skip to content

WebSocket

WebSocket is a protocol for full-duplex communication over a single TCP connection. A client initiates an HTTP Upgrade handshake (101 Switching Protocols), and after that both sides can send messages at any time without re-establishing the connection. Tornado has native WebSocket support, making it well-suited for building chat rooms, real-time data feeds, multi-user collaboration, and similar applications.

This article introduces the core interface of Tornado’s WebSocketHandler and demonstrates its use through two complete examples: a basic echo connection and a multi-user chat room.

WebSocketHandler Core Interface

Inherit from tornado.websocket.WebSocketHandler and override the following methods to handle WebSocket connections:

MethodWhen it fires
open(*args, **kwargs)WebSocket handshake succeeds; connection is established
on_message(message)A message is received from the client (text is str, binary is bytes)
on_close()The connection is closed (can be triggered by either client or server)
on_ping(data)A Ping frame is received from the client (optional override)
on_pong(data)A Pong frame is received from the client (optional override)

Sending messages and closing the connection:

MethodDescription
write_message(message, binary=False)Send a message to the client (async; use await)
close(code=None, reason=None)Actively close the connection
ping(data)Send a Ping frame

Other commonly used attributes:

  • self.close_code / self.close_reason: Status code and reason when the connection closes
  • self.request: The HTTP request object from the handshake (you can read Headers, Cookies, etc.)

Cross-Origin Handling

When making a WebSocket handshake, browsers include an Origin header. By default, Tornado only allows requests from the same domain. To allow cross-origin connections, override check_origin():

class MyWebSocketHandler(tornado.websocket.WebSocketHandler):
    def check_origin(self, origin):
        return True   # Allow all origins (development environment only)
        # In production, check against an origin whitelist

Basic Example

Server:

import tornado.ioloop
import tornado.web
import tornado.websocket

class EchoWebSocket(tornado.websocket.WebSocketHandler):
    def check_origin(self, origin):
        return True

    async def open(self):
        print(f"New connection: {self.request.remote_ip}")
        await self.write_message("Welcome to the Echo service!")

    async def on_message(self, message):
        print(f"Received: {message}")
        await self.write_message(f"You sent: {message}")

    def on_close(self):
        print(f"Connection closed, code={self.close_code}")

def make_app():
    return tornado.web.Application([
        (r"/ws", EchoWebSocket),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

Client (browser JavaScript):

const ws = new WebSocket("ws://localhost:8888/ws");

ws.onopen = () => {
    console.log("Connected");
    ws.send("Hello, Tornado!");
};

ws.onmessage = (event) => {
    console.log("Received:", event.data);
};

ws.onerror = (error) => {
    console.error("Connection error:", error);
};

ws.onclose = (event) => {
    console.log(`Connection closed, code=${event.code}`);
};

Chat Room Example

The core of a chat room is maintaining a global set of online users and broadcasting each new message to all of them.

Server

import tornado.ioloop
import tornado.web
import tornado.websocket

class ChatHandler(tornado.websocket.WebSocketHandler):
    users = set()   # All active connections

    def check_origin(self, origin):
        return True

    def open(self):
        ChatHandler.users.add(self)
        self.nickname = self.get_argument("name", f"User{id(self) % 10000}")
        self.broadcast(f"[System] {self.nickname} joined the chat room")

    async def on_message(self, message):
        self.broadcast(f"[{self.nickname}] {message}")

    def on_close(self):
        ChatHandler.users.discard(self)
        self.broadcast(f"[System] {self.nickname} left the chat room")

    def broadcast(self, message):
        for user in ChatHandler.users:
            try:
                user.write_message(message)
            except tornado.websocket.WebSocketClosedError:
                ChatHandler.users.discard(user)

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("chat.html")

def make_app():
    return tornado.web.Application(
        [
            (r"/", IndexHandler),
            (r"/ws", ChatHandler),
        ],
        template_path="templates",
        debug=True,
    )

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    print("Chat room started: http://localhost:8888")
    tornado.ioloop.IOLoop.current().start()

Client Page templates/chat.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Tornado Chat Room</title>
  <style>
    #messages { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 8px; }
    #input-row { display: flex; gap: 8px; margin-top: 8px; }
    #msg-input { flex: 1; }
  </style>
</head>
<body>
  <h2>Tornado Chat Room</h2>
  <div id="messages"></div>
  <div id="input-row">
    <input id="msg-input" type="text" placeholder="Type a message..." autofocus>
    <button onclick="sendMessage()">Send</button>
  </div>

  <script>
    const name = prompt("Enter your nickname:") || "Anonymous";
    const ws = new WebSocket(`ws://${location.host}/ws?name=${encodeURIComponent(name)}`);
    const messages = document.getElementById("messages");
    const input = document.getElementById("msg-input");

    ws.onmessage = (event) => {
        const div = document.createElement("div");
        div.textContent = event.data;
        messages.appendChild(div);
        messages.scrollTop = messages.scrollHeight;
    };

    ws.onclose = () => {
        const div = document.createElement("div");
        div.textContent = "[Connection closed]";
        div.style.color = "gray";
        messages.appendChild(div);
    };

    function sendMessage() {
        const text = input.value.trim();
        if (text && ws.readyState === WebSocket.OPEN) {
            ws.send(text);
            input.value = "";
        }
    }

    input.addEventListener("keydown", (e) => {
        if (e.key === "Enter") sendMessage();
    });
  </script>
</body>
</html>

Heartbeat / Keep-Alive

When no data is transmitted for an extended period, intermediate network devices (proxies, NAT) may drop the connection. Use the Ping/Pong mechanism to keep the connection alive:

import asyncio

class ChatHandler(tornado.websocket.WebSocketHandler):
    HEARTBEAT_INTERVAL = 30   # seconds

    def open(self):
        self._heartbeat = asyncio.get_event_loop().call_later(
            self.HEARTBEAT_INTERVAL, self._send_ping
        )

    def _send_ping(self):
        try:
            self.ping(b"ping")
            self._heartbeat = asyncio.get_event_loop().call_later(
                self.HEARTBEAT_INTERVAL, self._send_ping
            )
        except tornado.websocket.WebSocketClosedError:
            pass

    def on_close(self):
        if hasattr(self, "_heartbeat"):
            self._heartbeat.cancel()
write_message() is a coroutine. In high-concurrency broadcast scenarios, use asyncio.gather() to send messages in parallel rather than awaiting each one serially, to avoid blocking.
Last updated on