Build a WebSocket server
Build a WebSocket server using Durable Objects and Workers.
This example shows how to build a WebSocket server using Durable Objects and Workers. The example exposes an endpoint to create a new WebSocket connection. This WebSocket connection echos any message while including the total number of WebSocket connections currently established. For more information, refer to Use Durable Objects with WebSockets.
import { DurableObject } from "cloudflare:workers";
// Workerexport default { async fetch(request, env, ctx) { if (request.url.endsWith("/websocket")) { // Expect to receive a WebSocket Upgrade request. // If there is one, accept the request and return a WebSocket Response. const upgradeHeader = request.headers.get("Upgrade"); if (!upgradeHeader || upgradeHeader !== "websocket") { return new Response("Worker expected Upgrade: websocket", { status: 426, }); }
if (request.method !== "GET") { return new Response("Worker expected GET method", { status: 400, }); }
// Since we are hard coding the Durable Object ID by providing the constant name 'foo', // all requests to this Worker will be sent to the same Durable Object instance. let id = env.WEBSOCKET_SERVER.idFromName("foo"); let stub = env.WEBSOCKET_SERVER.get(id);
return stub.fetch(request); }
return new Response( `Supported endpoints:/websocket: Expects a WebSocket upgrade request`, { status: 200, headers: { "Content-Type": "text/plain", }, }, ); },};
// Durable Objectexport class WebSocketServer extends DurableObject { // Keeps track of all WebSocket connections sessions;
constructor(ctx, env) { super(ctx, env); this.sessions = new Map(); }
async fetch(request) { // Creates two ends of a WebSocket connection. const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair);
// Calling `accept()` tells the runtime that this WebSocket is to begin terminating // request within the Durable Object. It has the effect of "accepting" the connection, // and allowing the WebSocket to send and receive messages. server.accept();
// Generate a random UUID for the session. const id = crypto.randomUUID(); // Add the WebSocket connection to the map of active sessions. this.sessions.set(server, { id });
server.addEventListener("message", (event) => { this.handleWebSocketMessage(server, event.data); });
// If the client closes the connection, the runtime will close the connection too. server.addEventListener("close", () => { this.handleConnectionClose(server); });
return new Response(null, { status: 101, webSocket: client, }); }
async handleWebSocketMessage(ws, message) { const connection = this.sessions.get(ws);
// Reply back with the same message to the connection ws.send( `[Durable Object] message: ${message}, from: ${connection.id}, to: the initiating client. Total connections: ${this.sessions.size}`, );
// Broadcast the message to all the connections, // except the one that sent the message. this.sessions.forEach((_, session) => { if (session !== ws) { session.send( `[Durable Object] message: ${message}, from: ${connection.id}, to: all clients except the initiating client. Total connections: ${this.sessions.size}`, ); } });
// Broadcast the message to all the connections, // including the one that sent the message. this.sessions.forEach((_, session) => { session.send( `[Durable Object] message: ${message}, from: ${connection.id}, to: all clients. Total connections: ${this.sessions.size}`, ); }); }
async handleConnectionClose(ws) { this.sessions.delete(ws); ws.close(1000, "Durable Object is closing WebSocket"); }}import { DurableObject } from 'cloudflare:workers';
// Workerexport default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { if (request.url.endsWith('/websocket')) { // Expect to receive a WebSocket Upgrade request. // If there is one, accept the request and return a WebSocket Response. const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { return new Response('Worker expected Upgrade: websocket', { status: 426, }); }
if (request.method !== 'GET') { return new Response('Worker expected GET method', { status: 400, }); }
// Since we are hard coding the Durable Object ID by providing the constant name 'foo', // all requests to this Worker will be sent to the same Durable Object instance. let id = env.WEBSOCKET_SERVER.idFromName('foo'); let stub = env.WEBSOCKET_SERVER.get(id);
return stub.fetch(request); }
return new Response( `Supported endpoints:/websocket: Expects a WebSocket upgrade request`, { status: 200, headers: { 'Content-Type': 'text/plain', }, } ); },};
// Durable Objectexport class WebSocketServer extends DurableObject { // Keeps track of all WebSocket connections sessions: Map<WebSocket, { [key: string]: string }>;
constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.sessions = new Map(); }
async fetch(request: Request): Promise<Response> { // Creates two ends of a WebSocket connection. const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair);
// Calling `accept()` tells the runtime that this WebSocket is to begin terminating // request within the Durable Object. It has the effect of "accepting" the connection, // and allowing the WebSocket to send and receive messages. server.accept();
// Generate a random UUID for the session. const id = crypto.randomUUID(); // Add the WebSocket connection to the map of active sessions. this.sessions.set(server, { id });
server.addEventListener('message', (event) => { this.handleWebSocketMessage(server, event.data); });
// If the client closes the connection, the runtime will close the connection too. server.addEventListener('close', () => { this.handleConnectionClose(server); });
return new Response(null, { status: 101, webSocket: client, }); }
async handleWebSocketMessage(ws: WebSocket, message: string | ArrayBuffer) { const connection = this.sessions.get(ws)!;
// Reply back with the same message to the connection ws.send(`[Durable Object] message: ${message}, from: ${connection.id}, to: the initiating client. Total connections: ${this.sessions.size}`);
// Broadcast the message to all the connections, // except the one that sent the message. this.sessions.forEach((_, session) => { if (session !== ws) { session.send(`[Durable Object] message: ${message}, from: ${connection.id}, to: all clients except the initiating client. Total connections: ${this.sessions.size}`); } });
// Broadcast the message to all the connections, // including the one that sent the message. this.sessions.forEach((_, session) => { session.send(`[Durable Object] message: ${message}, from: ${connection.id}, to: all clients. Total connections: ${this.sessions.size}`); }); }
async handleConnectionClose(ws: WebSocket) { this.sessions.delete(ws); ws.close(1000, 'Durable Object is closing WebSocket'); }}from workers import DurableObject, Response, WorkerEntrypointfrom js import WebSocketPairfrom pyodide.ffi import create_proxyimport uuid
class Session: def __init__(self, *, ws): self.ws = ws
# Workerclass Default(WorkerEntrypoint): async def fetch(self, request): if request.url.endswith('/websocket'): # Expect to receive a WebSocket Upgrade request. # If there is one, accept the request and return a WebSocket Response. upgrade_header = request.headers.get('Upgrade') if not upgrade_header or upgrade_header != 'websocket': return Response('Worker expected Upgrade: websocket', status=426)
if request.method != 'GET': return Response('Worker expected GET method', status=400)
# Since we are hard coding the Durable Object ID by providing the constant name 'foo', # all requests to this Worker will be sent to the same Durable Object instance. id = self.env.WEBSOCKET_SERVER.idFromName('foo') stub = self.env.WEBSOCKET_SERVER.get(id)
return await stub.fetch(request)
return Response( """Supported endpoints:/websocket: Expects a WebSocket upgrade request""", status=200, headers={'Content-Type': 'text/plain'} )
# Durable Objectclass WebSocketServer(DurableObject): def __init__(self, ctx, env): super().__init__(ctx, env) # Keeps track of all WebSocket connections, keyed by session ID self.sessions = {}
async def fetch(self, request): # Creates two ends of a WebSocket connection. client, server = WebSocketPair.new().object_values()
# Calling `accept()` tells the runtime that this WebSocket is to begin terminating # request within the Durable Object. It has the effect of "accepting" the connection, # and allowing the WebSocket to send and receive messages. server.accept()
# Generate a random UUID for the session. id = str(uuid.uuid4())
# Create proxies for event handlers (must be destroyed when socket closes) async def on_message(event): await self.handleWebSocketMessage(id, event.data)
message_proxy = create_proxy(on_message) server.addEventListener('message', message_proxy)
# If the client closes the connection, the runtime will close the connection too. async def on_close(event): await self.handleConnectionClose(id) # Clean up proxies message_proxy.destroy() close_proxy.destroy()
close_proxy = create_proxy(on_close) server.addEventListener('close', close_proxy)
# Add the WebSocket connection to the map of active sessions, keyed by session ID. self.sessions[id] = Session(ws=server)
return Response(None, status=101, web_socket=client)
async def handleWebSocketMessage(self, session_id, message): session = self.sessions[session_id]
# Reply back with the same message to the connection session.ws.send(f"[Durable Object] message: {message}, from: {session_id}, to: the initiating client. Total connections: {len(self.sessions)}")
# Broadcast the message to all the connections, # except the one that sent the message. for id, conn in self.sessions.items(): if id != session_id: conn.ws.send(f"[Durable Object] message: {message}, from: {session_id}, to: all clients except the initiating client. Total connections: {len(self.sessions)}")
# Broadcast the message to all the connections, # including the one that sent the message. for id, conn in self.sessions.items(): conn.ws.send(f"[Durable Object] message: {message}, from: {session_id}, to: all clients. Total connections: {len(self.sessions)}")
async def handleConnectionClose(self, session_id): session = self.sessions.pop(session_id, None) if session: session.ws.close(1000, 'Durable Object is closing WebSocket')Finally, configure your Wrangler file to include a Durable Object binding and migration based on the namespace and class name chosen previously.
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "websocket-server", "main": "src/index.ts", "durable_objects": { "bindings": [ { "name": "WEBSOCKET_SERVER", "class_name": "WebSocketServer" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": [ "WebSocketServer" ] } ]}name = "websocket-server"main = "src/index.ts"
[[durable_objects.bindings]]name = "WEBSOCKET_SERVER"class_name = "WebSocketServer"
[[migrations]]tag = "v1"new_sqlite_classes = ["WebSocketServer"]Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-