← Cloudflare Durable Objects / durable-objects / best-practices
Используйте WebSocket
Durable Objects могут выступать в роли WebSocket-серверов и подключать тысячи клиентов на один экземпляр. Вы также можете использовать WebSocket в качестве клиента для подключения к другим серверам или Durable Objects.
Доступны два API WebSocket:
- Hibernation WebSocket API - Позволяет Durable Object переходить в гибернацию, не отключая клиентов при простое. (рекомендуется)
- Стандартный веб-API WebSocket - Использует знакомый
addEventListenerпаттерн события.
Что такое WebSocket?
WebSocket представляют собой долгоживущие TCP-соединения, которые обеспечивают двустороннюю связь в реальном времени между клиентом и сервером.
Ключевые характеристики:
- И Workers, и Durable Objects могут выступать конечными точками WebSocket, как клиент, так и сервер
- Долгоживущие сессии WebSocket делают Durable Objects идеальным решением для приема подключений
- Один экземпляр Durable Object может координировать работу нескольких клиентов (например, чатов или многопользовательских игр)
См. Cloudflare Edge Chat Demo ↗ с примером использования Durable Objects вместе с WebSocket.
Зачем использовать Hibernation?
Hibernation WebSocket API снижает расходы, позволяя Durable Objects переходить в спящий режим при простое:
- Клиенты остаются подключёнными, даже когда Durable Object не находится в памяти
- Тарификация по Billable Duration (GB-s) не начисляются во время гибернации
- Когда приходит сообщение, Durable Object автоматически просыпается
Durable Objects Hibernation WebSocket API
Hibernation WebSocket API расширяет Стандартный веб-API WebSocket чтобы снизить расходы в периоды бездействия.
Как работает гибернация
Если Durable Object некоторое время не получает событий (например, alarm или сообщений), он выгружается из памяти. Во время гибернации:
- Клиенты WebSocket остаются подключенными к сети Cloudflare
- Состояние в памяти сбрасывается
- Когда поступает событие, Durable Object повторно инициализируется, и его
constructorвыполняется
Чтобы восстановить состояние после выхода из спящего режима, используйте serializeAttachment и deserializeAttachment чтобы сохранять данные для каждого WebSocket-соединения.
См. Жизненный цикл Durable Object, где это описано подробнее.
Пример гибернации
Чтобы использовать WebSocket с Durable Objects:
- Проксируйте запрос от Worker к Durable Object
- Вызовите
DurableObjectState::acceptWebSocketчтобы принять соединение на стороне сервера - Определите методы обработчиков для нужных событий в классе Durable Object
Если для Durable Object, находящегося в гибернации, происходит событие, среда выполнения повторно инициализирует его, вызывая конструктор. При использовании гибернации сводите объём работы в конструкторе к минимуму.
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)Настройте файл Wrangler для работы с Durable Object привязка и миграция:
{
"$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" ]Полный пример доступен в Создание сервера WebSocket с WebSocket Hibernation.
Автоматическая обработка ping/pong
Среда выполнения Cloudflare автоматически обрабатывает ping-кадры протокола WebSocket:
- Входящий ping-кадры ↗ получают автоматические ответы pong
- Обработка ping/pong не прерывает переход в спящий режим
-
webSocketMessage-обработчик не вызывается для управляющих кадров
Благодаря этому соединения остаются активными, а Durable Object не просыпается.
Группируйте сообщения, чтобы снизить накладные расходы
Каждое сообщение WebSocket создаёт дополнительную нагрузку на обработку из-за переключений контекста между средой выполнения JavaScript и базовой системой. Отправка большого количества мелких сообщений может перегрузить отдельный Durable Object. Это происходит, даже если общий объём данных невелик.
Чтобы максимизировать пропускную способность:
- Группируйте несколько логических сообщений в один WebSocket-фрейм
- Используйте простой формат конверта для упаковки и распаковки пакетных сообщений
- Стремитесь к меньшему числу более крупных сообщений а не множество мелких
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
}
}Чем полезна группировка запросов
Чтение WebSocket требует переключения контекста между ядром и средой выполнения JavaScript. Каждое отдельное сообщение вызывает такие накладные расходы. Группировка 10-100 логических сообщений в один кадр WebSocket пропорционально снижает количество переключений контекста.
Для высокочастотных данных, например показаний датчиков или обновлений состояния игры, используйте группировку по времени или по количеству. Группируйте каждые 50-100ms или каждые 50-100 сообщений, в зависимости от того, что наступит раньше.
Расширенные методы
В Hibernation WebSocket API доступны следующие методы. Используйте их для сохранения и восстановления состояния до и после гибернации.
WebSocket.serializeAttachment
serializeAttachment(value:any)void
Хранит копию value связанный с WebSocket-соединением.
Основные особенности поведения:
- Сериализованные вложения сохраняются во время гибернации, пока WebSocket остаётся в рабочем состоянии
- Если любая из сторон закрывает соединение, вложения теряются
- Изменения в
valueпосле вызова этого метода не сохраняются, если вы не вызовете его снова -
valueможет быть любого типа, поддерживаемого алгоритм структурного клонирования ↗ - Максимальный размер сериализованных данных составляет 16,384 байт
Для больших значений или данных, которые должны сохраняться дольше времени жизни WebSocket, используйте Storage API и сохранить соответствующий ключ как вложение.
WebSocket.deserializeAttachment
deserializeAttachment():any
Возвращает последнее значение, переданное в serializeAttachment(), или null, если такого не существует.
Пример подключения
Используйте serializeAttachment и deserializeAttachment чтобы сохранять состояние отдельных соединений в периоды 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`);
// 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";
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`);
// 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);
}
}Стандартный API WebSocket
Подключения WebSocket устанавливаются путем отправки HTTP GET запроса с Upgrade: websocket заголовок.
Типичный порядок действий:
- Worker проверяет запрос на upgrade соединения
- Worker проксирует запрос к Durable Object
- Durable Object принимает серверное соединение
- Worker возвращает клиентское соединение в ответе
// 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",
},
});
},
};// Worker
export 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",
},
)Следующий Durable Object создаёт соединение WebSocket и отвечает на сообщения общим числом соединений:
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}`,
);
});
// When the client closes the connection, clean up the server side.
// 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.
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 Object
export 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}`,
);
});
// When the client closes the connection, clean up the server side.
// 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.
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, DurableObject
from js import WebSocketPair
from 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))
# When the client closes the connection, clean up the server side.
# 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.
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,
)Настройте файл Wrangler для работы с Durable Object привязка и миграция:
{
"$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" ]Полный пример доступен в Создание сервера WebSocket.
Дополнительные материалы
- Документация Mozilla Developer Network (MDN) по классу WebSocket ↗
- Шаблон WebSocket от Cloudflare для создания приложений на Workers с использованием WebSocket ↗
- базовый класс Durable Object
- Интерфейс Durable Object State