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.
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.
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 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.
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
-