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";
// Worker
export 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 Object
export 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);
});
// When the client closes the connection, clean up the server side.
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);
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(1000, "Durable Object is closing WebSocket");
}
}import { DurableObject } from 'cloudflare:workers';
// Worker
export 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 Object
export 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);
});
// When the client closes the connection, clean up the server side.
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);
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(1000, 'Durable Object is closing WebSocket');
}
}from workers import DurableObject, Response, WorkerEntrypoint
from js import WebSocketPair
from pyodide.ffi import create_proxy
import uuid
class Session:
def __init__(self, *, ws):
self.ws = ws
# Worker
class 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 Object
class 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)
# When the client closes the connection, clean up the server side.
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:
# With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
# auto-replies to Close frames. Calling close() is safe but no longer required.
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"
]
}
]
}"$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" ]