Skip to content
Cloudflare Docs

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:

  1. Hibernation WebSocket API - Allows the Durable Object to hibernate without disconnecting clients when idle. (recommended)
  2. Web Standard WebSocket API - Uses the familiar addEventListener event 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 constructor runs

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.

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 webSocketMessage handler is not called for control frames

This behavior keeps connections alive without waking the Durable Object.

Example

To use WebSockets with Durable Objects:

  1. Proxy the request from the Worker to the Durable Object
  2. Call DurableObjectState::acceptWebSocket to accept the server side connection
  3. 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.

JavaScript
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) {
// 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"]
}
]
}

A full example is available in Build a WebSocket server with WebSocket Hibernation.

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 value after calling this method are not retained unless you call it again
  • The value can 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.

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:

JavaScript
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);
}
}

WebSocket Standard API

WebSocket connections are established by making an HTTP GET request with the Upgrade: websocket header.

The typical flow:

  1. A Worker validates the upgrade request
  2. The Worker proxies the request to the Durable Object
  3. The Durable Object accepts the server side connection
  4. The Worker returns the client side connection in the response
JavaScript
// Worker
export 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",
},
});
},
};

The following Durable Object creates a WebSocket connection and responds to messages with the total number of connections:

JavaScript
import { DurableObject } from "cloudflare:workers";
// Durable Object
export 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,
});
}
}

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"]
}
]
}

A full example is available in Build a WebSocket server.