WebSockets
A Worker or a Durable Object accepts a WebSocket with WebSocketPair and a 101 response; a
Durable Object can accept it as hibernatable (ctx.acceptWebSocket()) and receive frames in
webSocketMessage(). Worker code can also open client sockets with new WebSocket(url).
{
"name": "wsecho",
"main": "index.js",
"compatibility_date": "2026-01-01",
"durable_objects": { "bindings": [{ "name": "W", "class_name": "W" }] },
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["W"] }]
}
export class W {
constructor(state, env) {
this.state = state;
}
async fetch(request) {
if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") {
return new Response("websocket upgrade required", { status: 426 });
}
const pair = new WebSocketPair();
const server = pair[0];
this.state.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: pair[1] });
}
async webSocketMessage(ws, msg) {
let count = (await this.state.storage.get("count")) ?? 0;
count++;
await this.state.storage.put("count", count);
ws.send(JSON.stringify({ echo: msg, count }));
}
}
export default {
async fetch(request, env) {
return env.W.get(env.W.idFromName("w")).fetch(request);
},
};
This is celld’s wsecho example (conformance/celld-examples/wsecho), copied
unchanged and run by the conformance suite on Node and in Chromium.
In a browser, a WebSocket is a MessagePort
A ServiceWorker cannot intercept WebSockets, so in a browser pages and objects get a WebSocket
class whose frames travel over MessagePorts (ADR 0006).
The WebSocket global of Worker code is workerd’s, built over the host’s: its prototype carries
serializeAttachment and deserializeAttachment, which partyserver and the Agents SDK use.
Where CellB matches workerd or celld
- Either end of a
WebSocketPairmaysend()beforeaccept()(celld queues the frame; workerd refuses). WebSocketPairends start withbinaryType"blob"when the project setswebsocket_standard_binary_type,"arraybuffer"otherwise.webSocketMessage()of a hibernatable socket always receives anArrayBuffer.- workerd’s
wd-websocket-hibernationtest passes on CellB; celld fails it.
How it is checked
celld’s wsecho and wsclient examples, CellB’s ws-details project and workerd’s WebSocket
tests (wd-websocket-*), on Node and in Chromium.
What breaks
- A class that extends the
WebSocketglobal gets a host socket, not an instance of itself:new WebSocket(url)returns the host’s own client socket. - A request whose
Upgradeheader vanished is answered as a plain request (typically426 Upgrade Required): a link that rebuilds requests must carryUpgradeacross (architecture.md). - Node’s
WebSockettakes'BLOB'as'blob', and Chromium’sCloseEventkeeps lone surrogates in a close reason (thewd-websocket-constructor-gapsknown gap).