Durable Objects は、1 インスタンスあたり数千のクライアントを接続できる WebSocket サーバーとして動作できます。他のサーバーや Durable Objects へ接続するクライアントとしても、WebSocket を使えます。
使える WebSocket API は 2 つです。
- Hibernation WebSocket API - アイドル時にクライアントを切断せず、Durable Object をハイバネーションできます。(推奨)
- Web Standard WebSocket API - よく知られた
addEventListenerのイベントパターンを使います。
WebSocket は、クライアントとサーバーのあいだで双方向のリアルタイム通信を可能にする、長寿命の TCP 接続です。
主な特徴は次のとおりです。
- Workers と Durable Objects の両方を、WebSocket のエンドポイント(クライアントまたはサーバー)にできます
- WebSocket セッションは長寿命なので、接続の受け入れ先として Durable Objects が適しています
- 1 つの Durable Object インスタンスで、複数クライアント間を調整できます(チャットルームやマルチプレイヤーゲームなど)
Durable Objects と WebSocket の例は、Cloudflare Edge Chat Demo ↗ を参照してください。
Hibernation WebSocket API は、アイドル時に Durable Objects をスリープさせることでコストを下げます。
- Durable Object がメモリになくても、クライアントは接続したままです
- ハイバネーション中は 課金対象の Duration(GB-s) は増えません
- メッセージが届くと、Durable Object は自動で起きます
Hibernation WebSocket API は、Web Standard WebSocket API を拡張し、非アクティブな期間のコストを下げます。
Durable Object がアラームやメッセージなどのイベントを短いあいだ受け取らないと、メモリから退去されます。ハイバネーション中は次のようになります。
- WebSocket クライアントは Cloudflare ネットワークへ接続したままです
- インメモリ状態はリセットされます
- イベントが届くと Durable Object が再初期化され、
constructorが実行されます
ハイバネーション後に状態を戻すには、serializeAttachment と deserializeAttachment を使い、各 WebSocket 接続にデータを永続化します。
詳細は Durable Object のライフサイクル を参照してください。
Durable Objects で WebSocket を使う手順は次のとおりです。
- 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 Hibernation で WebSocket サーバーを作る にあります。
Cloudflare ランタイムは、WebSocket プロトコルの ping フレームを自動で処理します。
- 受信した ping フレーム ↗ には、自動で pong が返ります
- ping/pong の処理はハイバネーションを中断しません
- 制御フレームでは
webSocketMessageハンドラーは呼ばれません
この動作により、Durable Object を起こさずに接続を維持できます。
WebSocket メッセージごとに、JavaScript ランタイムと基盤システム間のコンテキストスイッチによる処理オーバーヘッドが発生します。小さなメッセージを大量に送ると、1 つの Durable Object に負荷が集中します。データ量の合計が小さくても同じです。
スループットを最大化するには、次のようにします。
- 複数の論理メッセージをバッチして、1 つの 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 ランタイムのあいだでコンテキストスイッチが必要です。メッセージ 1 件ごとにこのオーバーヘッドが起きます。10〜100 件の論理メッセージを 1 つの WebSocket フレームにまとめると、コンテキストスイッチはそれに応じて減ります。
センサー読み取りやゲーム状態の更新のような高頻度データでは、時間ベースまたは件数ベースのバッチ処理を使います。50〜100ms ごと、または 50〜100 メッセージごと、先に達した方でまとめます。
Hibernation WebSocket API では、次のメソッドが使えます。ハイバネーションの前後で状態を永続化および復元するために使います。
serializeAttachment(value:any)void
value のコピーを、その WebSocket 接続に関連付けて保持します。
主な動作は次のとおりです。
- シリアライズした添付は、WebSocket が健全なあいだはハイバネーションをまたいで残ります
- どちらか一方が接続を閉じると、添付は失われます
- このメソッドを呼んだあとの
valueの変更は、再度呼ばない限り保持されません valueは structured clone algorithm ↗ がサポートする任意の型にできます- シリアライズ後の最大サイズは 16,384 バイトです
より大きな値や、WebSocket の生存期間を超えて残す必要があるデータは、Storage API を使い、対応するキーを添付として保存します。
deserializeAttachment():any
serializeAttachment() に渡した直近の値を取得します。なければ null です。
serializeAttachment と deserializeAttachment を使い、接続ごとの状態をハイバネーションをまたいで永続化します。
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);
}
}WebSocket 接続は、Upgrade: websocket ヘッダー付きの HTTP GET リクエストで確立します。
典型的な流れは次のとおりです。
- Worker がアップグレードリクエストを検証します
- 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 サーバーを作る にあります。