INTEGRITY Документация

Используйте WebSocket

Durable Objects могут выступать в роли WebSocket-серверов и подключать тысячи клиентов на один экземпляр. Вы также можете использовать WebSocket в качестве клиента для подключения к другим серверам или Durable Objects.

Доступны два API WebSocket:

  1. Hibernation WebSocket API - Позволяет Durable Object переходить в гибернацию, не отключая клиентов при простое. (рекомендуется)
  2. Стандартный веб-API WebSocket - Использует знакомый addEventListener паттерн события.

Что такое WebSocket?

WebSocket представляют собой долгоживущие TCP-соединения, которые обеспечивают двустороннюю связь в реальном времени между клиентом и сервером.

Ключевые характеристики:

См. Cloudflare Edge Chat Demo ↗ с примером использования Durable Objects вместе с WebSocket.

Зачем использовать Hibernation?

Hibernation WebSocket API снижает расходы, позволяя Durable Objects переходить в спящий режим при простое:

Durable Objects Hibernation WebSocket API

Hibernation WebSocket API расширяет Стандартный веб-API WebSocket чтобы снизить расходы в периоды бездействия.

Как работает гибернация

Если Durable Object некоторое время не получает событий (например, alarm или сообщений), он выгружается из памяти. Во время гибернации:

Чтобы восстановить состояние после выхода из спящего режима, используйте serializeAttachment и deserializeAttachment чтобы сохранять данные для каждого WebSocket-соединения.

См. Жизненный цикл Durable Object, где это описано подробнее.

Пример гибернации

Чтобы использовать WebSocket с Durable Objects:

  1. Проксируйте запрос от Worker к Durable Object
  2. Вызовите DurableObjectState::acceptWebSocket чтобы принять соединение на стороне сервера
  3. Определите методы обработчиков для нужных событий в классе 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:

Благодаря этому соединения остаются активными, а Durable Object не просыпается.

Группируйте сообщения, чтобы снизить накладные расходы

Каждое сообщение WebSocket создаёт дополнительную нагрузку на обработку из-за переключений контекста между средой выполнения JavaScript и базовой системой. Отправка большого количества мелких сообщений может перегрузить отдельный Durable Object. Это происходит, даже если общий объём данных невелик.

Чтобы максимизировать пропускную способность:

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

Хранит копию value связанный с WebSocket-соединением.

Основные особенности поведения:

Для больших значений или данных, которые должны сохраняться дольше времени жизни WebSocket, используйте Storage API и сохранить соответствующий ключ как вложение.

WebSocket.deserializeAttachment

Возвращает последнее значение, переданное в 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 заголовок.

Типичный порядок действий:

  1. Worker проверяет запрос на upgrade соединения
  2. Worker проксирует запрос к Durable Object
  3. Durable Object принимает серверное соединение
  4. 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.