Use WebSockets
Durable Objects can act as WebSocket servers that connect thousands of clients per instance. You can also use WebSockets as a client to connect to other servers or Durable Objects.
Two WebSocket APIs are available:
- Hibernation WebSocket API - Allows the Durable Object to hibernate without disconnecting clients when idle. (recommended)
- Web Standard WebSocket API - Uses the familiar
addEventListenerevent pattern.
WebSockets are long-lived TCP connections that enable bi-directional, real-time communication between client and server.
Key characteristics:
- Both Workers and Durable Objects can act as WebSocket endpoints (client or server)
- WebSocket sessions are long-lived, making Durable Objects ideal for accepting connections
- A single Durable Object instance can coordinate between multiple clients (for example, chat rooms or multiplayer games)
Refer to Cloudflare Edge Chat Demo ↗ for an example of using Durable Objects with WebSockets.
The Hibernation WebSocket API reduces costs by allowing Durable Objects to sleep when idle:
- Clients remain connected while the Durable Object is not in memory
- Billable Duration (GB-s) charges do not accrue during hibernation
- When a message arrives, the Durable Object wakes up automatically
The Hibernation WebSocket API extends the Web Standard WebSocket API to reduce costs during periods of inactivity.
When a Durable Object receives no events (such as alarms or messages) for a short period, it is evicted from memory. During hibernation:
- WebSocket clients remain connected to the Cloudflare network
- In-memory state is reset
- When an event arrives, the Durable Object is re-initialized and its
constructorruns
To restore state after hibernation, use serializeAttachment and deserializeAttachment to persist data with each WebSocket connection.
Refer to Lifecycle of a Durable Object for more information.
To use WebSockets with Durable Objects:
- Proxy the request from the Worker to the Durable Object
- Call
DurableObjectState::acceptWebSocketto accept the server side connection - Define handler methods on the Durable Object class for relevant events
If an event occurs for a hibernated Durable Object, the runtime re-initializes it by calling the constructor. Minimize work in the constructor when using hibernation.
import { DurableObject } from "cloudflare:workers";
// Durable Objectexport class WebSocketHibernationServer extends DurableObject { async fetch(request) { // Creates two ends of a WebSocket connection. const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair);
// Calling `acceptWebSocket()` connects the WebSocket to the Durable Object, allowing the WebSocket to send and receive messages. // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` allows the Durable Object to be hibernated // When the Durable Object receives a message during Hibernation, it will run the `constructor` to be re-initialized this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client, }); }
async webSocketMessage(ws, message) { // Upon receiving a message from the client, reply with the same message, // but will prefix the message with "[Durable Object]: " and return the number of connections. ws.send( `[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`, ); }
async webSocketClose(ws, code, reason, wasClean) { // Calling close() on the server completes the WebSocket close handshake ws.close(code, reason); }}import { DurableObject } from "cloudflare:workers";
export interface Env { WEBSOCKET_HIBERNATION_SERVER: DurableObjectNamespace<WebSocketHibernationServer>;}
// Durable Objectexport class WebSocketHibernationServer extends DurableObject { async fetch(request: Request): Promise<Response> { // Creates two ends of a WebSocket connection. const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair);
// Calling `acceptWebSocket()` connects the WebSocket to the Durable Object, allowing the WebSocket to send and receive messages. // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` allows the Durable Object to be hibernated // When the Durable Object receives a message during Hibernation, it will run the `constructor` to be re-initialized this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client, }); }
async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { // Upon receiving a message from the client, reply with the same message, // but will prefix the message with "[Durable Object]: " and return the number of connections. ws.send( `[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`, ); }
async webSocketClose( ws: WebSocket, code: number, reason: string, wasClean: boolean, ) { // Calling close() on the server completes the WebSocket close handshake ws.close(code, reason); }}from workers import Response, DurableObjectfrom js import WebSocketPair
# Durable Object
class WebSocketHibernationServer(DurableObject):def **init**(self, state, env):super().**init**(state, env)self.ctx = state
async def fetch(self, request): # Creates two ends of a WebSocket connection. client, server = WebSocketPair.new().object_values()
# Calling `acceptWebSocket()` connects the WebSocket to the Durable Object, allowing the WebSocket to send and receive messages. # Unlike `ws.accept()`, `state.acceptWebSocket(ws)` allows the Durable Object to be hibernated # When the Durable Object receives a message during Hibernation, it will run the `__init__` to be re-initialized self.ctx.acceptWebSocket(server)
return Response( None, status=101, web_socket=client )
async def webSocketMessage(self, ws, message): # Upon receiving a message from the client, reply with the same message, # but will prefix the message with "[Durable Object]: " and return the number of connections. ws.send( f"[Durable Object] message: {message}, connections: {len(self.ctx.get_websockets())}" )
async def webSocketClose(self, ws, code, reason, was_clean): # Calling close() on the server completes the WebSocket close handshake ws.close(code, reason)Configure your Wrangler file with a Durable Object binding and migration:
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "websocket-hibernation-server", "durable_objects": { "bindings": [ { "name": "WEBSOCKET_HIBERNATION_SERVER", "class_name": "WebSocketHibernationServer" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["WebSocketHibernationServer"] } ]}"$schema" = "./node_modules/wrangler/config-schema.json"name = "websocket-hibernation-server"
[[durable_objects.bindings]]name = "WEBSOCKET_HIBERNATION_SERVER"class_name = "WebSocketHibernationServer"
[[migrations]]tag = "v1"new_sqlite_classes = [ "WebSocketHibernationServer" ]A full example is available in Build a WebSocket server with WebSocket Hibernation.
The Cloudflare runtime automatically handles WebSocket protocol ping frames:
- Incoming ping frames ↗ receive automatic pong responses
- Ping/pong handling does not interrupt hibernation
- The
webSocketMessagehandler is not called for control frames
This behavior keeps connections alive without waking the Durable Object.
Each WebSocket message incurs processing overhead from context switches between the JavaScript runtime and the underlying system. Sending many small messages can overwhelm a single Durable Object. This happens even if the total data volume is small.
To maximize throughput:
- Batch multiple logical messages into a single WebSocket frame
- Use a simple envelope format to pack and unpack batched messages
- Target fewer, larger messages rather than many small ones
import { DurableObject } from "cloudflare:workers";
// Define a batch envelope format// Client-side: batch messages before sendingfunction sendBatch(ws, messages) { const batch = { messages, timestamp: Date.now(), }; ws.send(JSON.stringify(batch));}
// Durable Object: process batched messagesexport class GameRoom extends DurableObject { async webSocketMessage(ws, message) { if (typeof message !== "string") return;
const batch = JSON.parse(message);
// Process all messages in the batch in a single handler invocation for (const msg of batch.messages) { this.handleMessage(ws, msg); } }
handleMessage(ws, msg) { // Handle individual message logic }}import { DurableObject } from "cloudflare:workers";
// Define a batch envelope formatinterface BatchedMessage {messages: Array<{ type: string; payload: unknown }>;timestamp: number;}
// Client-side: batch messages before sendingfunction sendBatch(ws: WebSocket,messages: Array<{ type: string; payload: unknown }>,) {const batch: BatchedMessage = {messages,timestamp: Date.now(),};ws.send(JSON.stringify(batch));}
// Durable Object: process batched messagesexport class GameRoom extends DurableObject<Env> {async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {if (typeof message !== "string") return;
const batch = JSON.parse(message) as BatchedMessage;
// Process all messages in the batch in a single handler invocation for (const msg of batch.messages) { this.handleMessage(ws, msg); } }
private handleMessage(ws: WebSocket, msg: { type: string; payload: unknown }) { // Handle individual message logic }
}WebSocket reads require context switches between the kernel and JavaScript runtime. Each individual message triggers this overhead. Batching 10-100 logical messages into a single WebSocket frame reduces context switches proportionally.
For high-frequency data like sensor readings or game state updates, use time-based or count-based batching. Batch every 50-100ms or every 50-100 messages, whichever comes first.
The following methods are available on the Hibernation WebSocket API. Use them to persist and restore state before and after hibernation.
-
: voidserializeAttachment(value any)
Keeps a copy of value associated with the WebSocket connection.
Key behaviors:
- Serialized attachments persist through hibernation as long as the WebSocket remains healthy
- If either side closes the connection, attachments are lost
- Modifications to
valueafter calling this method are not retained unless you call it again - The
valuecan be any type supported by the structured clone algorithm ↗ - Maximum serialized size is 2,048 bytes
For larger values or data that must persist beyond WebSocket lifetime, use the Storage API and store the corresponding key as an attachment.
deserializeAttachment(): any
Retrieves the most recent value passed to serializeAttachment(), or null if none exists.
Use serializeAttachment and deserializeAttachment to persist per-connection state across hibernation:
import { DurableObject } from "cloudflare:workers";
export class WebSocketServer extends DurableObject { async fetch(request) { const url = new URL(request.url); const orderId = url.searchParams.get("orderId") ?? "anonymous";
const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair);
this.ctx.acceptWebSocket(server);
// Persist per-connection state that survives hibernation const state = { orderId, joinedAt: Date.now(), }; server.serializeAttachment(state);
return new Response(null, { status: 101, webSocket: client }); }
async webSocketMessage(ws, message) { // Restore state after potential hibernation const state = ws.deserializeAttachment(); ws.send(`Hello ${state.orderId}, you joined at ${state.joinedAt}`); }
async webSocketClose(ws, code, reason, wasClean) { const state = ws.deserializeAttachment(); console.log(`${state.orderId} disconnected`); ws.close(code, reason); }}import { DurableObject } from "cloudflare:workers";
interface ConnectionState { orderId: string; joinedAt: number;}
export class WebSocketServer extends DurableObject<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const orderId = url.searchParams.get("orderId") ?? "anonymous";
const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair);
this.ctx.acceptWebSocket(server);
// Persist per-connection state that survives hibernation const state: ConnectionState = { orderId, joinedAt: Date.now(), }; server.serializeAttachment(state);
return new Response(null, { status: 101, webSocket: client }); }
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) { // Restore state after potential hibernation const state = ws.deserializeAttachment() as ConnectionState; ws.send(`Hello ${state.orderId}, you joined at ${state.joinedAt}`); }
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { const state = ws.deserializeAttachment() as ConnectionState; console.log(`${state.orderId} disconnected`); ws.close(code, reason); }}WebSocket connections are established by making an HTTP GET request with the Upgrade: websocket header.
The typical flow:
- A Worker validates the upgrade request
- The Worker proxies the request to the Durable Object
- The Durable Object accepts the server side connection
- The Worker returns the client side connection in the response
// Workerexport default { async fetch(request, env, ctx) { if (request.method === "GET" && 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(null, { status: 426, statusText: "Durable Object expected Upgrade: websocket", headers: { "Content-Type": "text/plain", }, }); }
// This example will refer to a single Durable Object instance, since the name "foo" is // hardcoded let stub = env.WEBSOCKET_SERVER.getByName("foo");
// The Durable Object's fetch handler will accept the server side connection and return // the client return stub.fetch(request); }
return new Response(null, { status: 400, statusText: "Bad Request", headers: { "Content-Type": "text/plain", }, }); },};// Workerexport default { async fetch(request, env, ctx): Promise<Response> { if (request.method === "GET" && 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(null, { status: 426, statusText: "Durable Object expected Upgrade: websocket", headers: { "Content-Type": "text/plain", }, }); }
// This example will refer to a single Durable Object instance, since the name "foo" is // hardcoded let stub = env.WEBSOCKET_SERVER.getByName("foo");
// The Durable Object's fetch handler will accept the server side connection and return // the client return stub.fetch(request); }
return new Response(null, { status: 400, statusText: "Bad Request", headers: { "Content-Type": "text/plain", }, }); },} satisfies ExportedHandler<Env>;from workers import Response, WorkerEntrypoint
# Worker
class Default(WorkerEntrypoint):async def fetch(self, request):if request.method == "GET" and 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(None,status=426,status_text="Durable Object expected Upgrade: websocket",headers={"Content-Type": "text/plain",},)
# This example will refer to a single Durable Object instance, since the name "foo" is # hardcoded stub = self.env.WEBSOCKET_SERVER.getByName("foo")
# The Durable Object's fetch handler will accept the server side connection and return # the client return await stub.fetch(request)
return Response( None, status=400, status_text="Bad Request", headers={ "Content-Type": "text/plain", }, )The following Durable Object creates a WebSocket connection and responds to messages with the total number of connections:
import { DurableObject } from "cloudflare:workers";
// Durable Objectexport class WebSocketServer extends DurableObject { currentlyConnectedWebSockets;
constructor(ctx, env) { super(ctx, env); this.currentlyConnectedWebSockets = 0; }
async fetch(request) { // Creates two ends of a WebSocket connection. const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair);
// Calling `accept()` connects the WebSocket to this Durable Object server.accept(); this.currentlyConnectedWebSockets += 1;
// Upon receiving a message from the client, the server replies with the same message, // and the total number of connections with the "[Durable Object]: " prefix server.addEventListener("message", (event) => { server.send( `[Durable Object] currentlyConnectedWebSockets: ${this.currentlyConnectedWebSockets}`, ); });
// If the client closes the connection, the runtime will close the connection too. server.addEventListener("close", (cls) => { this.currentlyConnectedWebSockets -= 1; server.close(cls.code, "Durable Object is closing WebSocket"); });
return new Response(null, { status: 101, webSocket: client, }); }}// Durable Objectexport class WebSocketServer extends DurableObject { currentlyConnectedWebSockets: number;
constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.currentlyConnectedWebSockets = 0; }
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()` connects the WebSocket to this Durable Object server.accept(); this.currentlyConnectedWebSockets += 1;
// Upon receiving a message from the client, the server replies with the same message, // and the total number of connections with the "[Durable Object]: " prefix server.addEventListener("message", (event: MessageEvent) => { server.send( `[Durable Object] currentlyConnectedWebSockets: ${this.currentlyConnectedWebSockets}`, ); });
// If the client closes the connection, the runtime will close the connection too. server.addEventListener("close", (cls: CloseEvent) => { this.currentlyConnectedWebSockets -= 1; server.close(cls.code, "Durable Object is closing WebSocket"); });
return new Response(null, { status: 101, webSocket: client, }); }}from workers import Response, DurableObjectfrom js import WebSocketPairfrom pyodide.ffi import create_proxy
# Durable Object
class WebSocketServer(DurableObject):def **init**(self, ctx, env):super().**init**(ctx, env)self.currently_connected_websockets = 0
async def fetch(self, request): # Creates two ends of a WebSocket connection. client, server = WebSocketPair.new().object_values()
# Calling `accept()` connects the WebSocket to this Durable Object server.accept() self.currently_connected_websockets += 1
# Upon receiving a message from the client, the server replies with the same message, # and the total number of connections with the "[Durable Object]: " prefix def on_message(event): server.send( f"[Durable Object] currentlyConnectedWebSockets: {self.currently_connected_websockets}" )
server.addEventListener("message", create_proxy(on_message))
# If the client closes the connection, the runtime will close the connection too. def on_close(event): self.currently_connected_websockets -= 1 server.close(event.code, "Durable Object is closing WebSocket")
server.addEventListener("close", create_proxy(on_close))
return Response( None, status=101, web_socket=client, )Configure your Wrangler file with a Durable Object binding and migration:
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "websocket-server", "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"
[[durable_objects.bindings]]name = "WEBSOCKET_SERVER"class_name = "WebSocketServer"
[[migrations]]tag = "v1"new_sqlite_classes = [ "WebSocketServer" ]A full example is available in Build a WebSocket server.
- Mozilla Developer Network's (MDN) documentation on the WebSocket class ↗
- Cloudflare's WebSocket template for building applications on Workers using WebSockets ↗
- Durable Object base class
- Durable Object State interface
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
- © 2026 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-