Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

WebSocket を使う

最終更新 Markdown で表示Agent セットアップ

Durable Objects は、1 インスタンスあたり数千のクライアントを接続できる WebSocket サーバーとして動作できます。他のサーバーや Durable Objects へ接続するクライアントとしても、WebSocket を使えます。

使える WebSocket API は 2 つです。

  1. Hibernation WebSocket API - アイドル時にクライアントを切断せず、Durable Object をハイバネーションできます。(推奨)
  2. Web Standard WebSocket API - よく知られた addEventListener のイベントパターンを使います。

WebSocket とは

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 は自動で起きます

Durable Objects の Hibernation WebSocket API

Hibernation WebSocket API は、Web Standard WebSocket API を拡張し、非アクティブな期間のコストを下げます。

ハイバネーションの仕組み

Durable Object がアラームやメッセージなどのイベントを短いあいだ受け取らないと、メモリから退去されます。ハイバネーション中は次のようになります。

  • WebSocket クライアントは Cloudflare ネットワークへ接続したままです
  • インメモリ状態はリセットされます
  • イベントが届くと Durable Object が再初期化され、constructor が実行されます

ハイバネーション後に状態を戻すには、serializeAttachmentdeserializeAttachment を使い、各 WebSocket 接続にデータを永続化します。

詳細は Durable Object のライフサイクル を参照してください。

ハイバネーションの例

Durable Objects で WebSocket を使う手順は次のとおりです。

  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 Hibernation で WebSocket サーバーを作る にあります。

ping/pong の自動処理

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 では、次のメソッドが使えます。ハイバネーションの前後で状態を永続化および復元するために使います。

WebSocket.serializeAttachment

  • serializeAttachment(value any) : void

value のコピーを、その WebSocket 接続に関連付けて保持します。

主な動作は次のとおりです。

  • シリアライズした添付は、WebSocket が健全なあいだはハイバネーションをまたいで残ります
  • どちらか一方が接続を閉じると、添付は失われます
  • このメソッドを呼んだあとの value の変更は、再度呼ばない限り保持されません
  • valuestructured clone algorithm がサポートする任意の型にできます
  • シリアライズ後の最大サイズは 16,384 バイトです

より大きな値や、WebSocket の生存期間を超えて残す必要があるデータは、Storage API を使い、対応するキーを添付として保存します。

WebSocket.deserializeAttachment

  • deserializeAttachment(): any

serializeAttachment() に渡した直近の値を取得します。なければ null です。

添付の例

serializeAttachmentdeserializeAttachment を使い、接続ごとの状態をハイバネーションをまたいで永続化します。

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 標準 API

WebSocket 接続は、Upgrade: websocket ヘッダー付きの HTTP GET リクエストで確立します。

典型的な流れは次のとおりです。

  1. Worker がアップグレードリクエストを検証します
  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 サーバーを作る にあります。

関連リソース

役に立ちましたか?