Use WebSockets
Last updated Sep 30, 2026 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.
What are WebSockets?
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.
Why use Hibernation?
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
Durable Objects Hibernation WebSocket API
The Hibernation WebSocket API extends the Web Standard WebSocket API to reduce costs during periods of inactivity.
How hibernation works
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.
Hibernation example
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 Object
export 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) {
// 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(code, reason);
}
}
import { DurableObject } from "cloudflare:workers";
export interface Env {
WEBSOCKET_HIBERNATION_SERVER: DurableObjectNamespace<WebSocketHibernationServer>;
}
// Durable Object
export 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,
) {
// 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(code, reason);
}
}
from workers import Response, DurableObject
from 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):
# 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(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.
Local development support
Prior to wrangler@3.13.2 and Miniflare v3.20231016.0, WebSockets did not hibernate in local development. Hibernatable WebSocket events like webSocketMessage() are still delivered. However, the Durable Object is never evicted from memory.
Automatic ping/pong handling
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.
Batch messages to reduce overhead
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 sending
function sendBatch(ws, messages) {
const batch = {
messages,
timestamp: Date.now(),
};
ws.send(JSON.stringify(batch));
}
// Durable Object: process batched messages
export 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 format
interface BatchedMessage {
messages: Array<{ type: string; payload: unknown }>;
timestamp: number;
}
// Client-side: batch messages before sending
function 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 messages
export 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
}
}
Why batching helps
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.
Note
Hibernation is only supported when a Durable Object acts as a WebSocket server. Outgoing WebSockets do not hibernate.
However, an open outbound WebSocket connection prevents eviction for up to 15 minutes. The connection itself may remain open after that. Starting another I/O operation later can extend the Durable Object's time in memory. For more information, refer to Lifecycle of a Durable Object.
Events such as alarms, incoming requests, and scheduled callbacks prevent hibernation. This includes setTimeout and setInterval usage. Read more about when a Durable Object incurs duration charges.
Extended methods
The following methods are available on the Hibernation WebSocket API. Use them to persist and restore state before and after hibernation.
WebSocket.serializeAttachment
serializeAttachment(value any):void
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 16,384 bytes
For larger values or data that must persist beyond WebSocket lifetime, use the Storage API and store the corresponding key as an attachment.
WebSocket.deserializeAttachment
deserializeAttachment():any
Retrieves the most recent value passed to serializeAttachment(), or null if none exists.
Attachment example
Use serializeAttachment and deserializeAttachment to persist per-connection state across hibernation:
```js 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);
