How to Implement WebSocket Servers in workerd: A Complete Guide
To implement a WebSocket server in workerd, detect the upgrade request, return a 101 response with webSocket: true, call accept() on the resulting WebSocket object, and use send() and receive() for bidirectional communication.
The workerd runtime (Cloudflare's open-source Workers platform) enables full-duplex WebSocket connections directly within HTTP request handlers. This guide covers the complete implementation pattern, from basic upgrade handling to advanced hibernation features, based on the actual source code in the cloudflare/workerd repository.
The WebSocket Upgrade Flow in workerd
The architecture follows a strict state machine defined in src/workerd/api/web-socket.c++. When a client requests a WebSocket upgrade, workerd processes the connection through three distinct phases:
- Detection – Identify the
Upgrade: websocketheader in the incoming request - Acceptance – Return a 101 response via
Response.acceptWebSocket()to obtain the server-sideWebSocketobject - Activation – Call
ws.accept()to transition fromAwaitingConnectiontoAcceptedstate before any I/O
The HTTP-to-WebSocket upgrade logic resides in src/workerd/api/http.c++ (around line 1213), where Response.acceptWebSocket() handles the protocol switch and returns the socket instance.
Basic WebSocket Server Implementation
Detecting Upgrade Requests
Check the Upgrade header to identify WebSocket handshake requests. While the C++ API provides headers.isWebSocket(), the JavaScript surface requires manual header inspection:
export default {
async fetch(request, env, ctx) {
const upgrade = request.headers.get('Upgrade');
if (!upgrade || upgrade.toLowerCase() !== 'websocket') {
return new Response('Expected WebSocket', { status: 400 });
}
// Proceed with upgrade...
}
};
Accepting the Connection
Create a 101 response with webSocket: true to trigger the upgrade mechanism. The respondWith pattern returns a Response containing the webSocket property:
export default {
async fetch(request, env, ctx) {
if (request.headers.get('Upgrade')?.toLowerCase() !== 'websocket') {
return new Response('Not a WebSocket request', { status: 400 });
}
// Create the upgrade response
const response = new Response(null, {
status: 101,
webSocket: true
});
// Obtain the server-side WebSocket object
const ws = (await request.respondWith(response)).webSocket;
// Mandatory: accept before any I/O
ws.accept();
// Now ready for bidirectional communication
return new Response(null, { status: 101 });
}
};
According to the implementation in src/workerd/api/web-socket.c++, calling ws.accept() transitions the internal state machine from AwaitingConnection to Accepted. Attempting to send or receive before this call throws an error.
Handling Messages
Use ws.receive() to read messages and ws.send() to transmit data. The receive() method resolves with either a string, a Uint8Array (for binary frames), or null when the client closes the connection:
export default {
async fetch(request, env, ctx) {
const upgrade = request.headers.get('Upgrade');
if (upgrade?.toLowerCase() !== 'websocket') {
return new Response('Expected WebSocket', { status: 400 });
}
const ws = (await request.respondWith(
new Response(null, { status: 101, webSocket: true })
)).webSocket;
ws.accept();
try {
while (true) {
const message = await ws.receive();
if (message === null) break; // Connection closed
// Echo server example
await ws.send(`Server received: ${message}`);
}
} finally {
await ws.close(1000, 'Normal closure');
}
return new Response(null, { status: 101 });
}
};
The WebSocket::receive() implementation in src/workerd/api/web-socket.c++ polls the underlying kj::WebSocket and wraps the result in V8 promises.
Advanced Patterns
Coupling WebSockets for Proxying
Forward traffic between two sockets using WebSocket.couple(), implemented in src/workerd/api/web-socket.c++ (lines 19-42). This creates bidirectional pumps that run until both connections close:
export default {
async fetch(request, env, ctx) {
// Accept client connection
const clientWs = (await request.respondWith(
new Response(null, { status: 101, webSocket: true })
)).webSocket;
clientWs.accept();
// Connect to backend
const backendResp = await fetch('wss://backend.example.com/socket', {
headers: { 'Upgrade': 'websocket' }
});
const backendWs = backendResp.webSocket;
backendWs.accept();
// Pipe both directions (returns when either side closes)
await clientWs.couple(backendWs, request);
return new Response(null, { status: 101 });
}
};
WebSocket Hibernation
For long-running connections in Durable Objects, enable hibernation to allow the actor to be evicted while maintaining the connection. Set the hibernatable option when accepting:
export class ChatRoom {
async fetch(request) {
const ws = (await request.respondWith(
new Response(null, { status: 101, webSocket: true })
)).webSocket;
// Enable hibernation support
ws.accept({ allowHalfOpen: true, hibernatable: true });
// Store in Durable Object state for persistence across hibernation
this.state.acceptWebSocket(ws);
return new Response(null, { status: 101 });
}
}
The hibernation pathway is managed in src/workerd/io/hibernation-manager.c++, specifically in HibernationManagerImpl::acceptWebSocket.
Testing with WebSocketPair
For unit testing, use WebSocketPair to create linked client-server sockets without network overhead:
import { WebSocketPair } from 'workerd';
const [client, server] = new WebSocketPair();
server.accept();
await client.send('test message');
const received = await server.receive();
console.assert(received === 'test message');
See src/workerd/api/tests/websocket-constructor-test.js for comprehensive test patterns.
Durable Objects and WebSocket Servers
The canonical example in samples/durable-objects-chat/chat.js demonstrates stateful WebSocket management. In Durable Objects, use state.acceptWebSocket() to register the socket with the object's hibernation manager:
export class ChatRoom {
constructor(state, env) {
this.state = state;
this.clients = new Set();
}
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/websocket') {
const ws = (await request.respondWith(
new Response(null, { status: 101, webSocket: true })
)).webSocket;
ws.accept();
this.clients.add(ws);
// Handle messages and broadcast
this.handleSession(ws);
return new Response(null, { status: 101 });
}
return new Response('Not found', { status: 404 });
}
async handleSession(ws) {
try {
while (true) {
const msg = await ws.receive();
if (msg === null) break;
// Broadcast to all connected clients
const promises = Array.from(this.clients).map(client =>
client.send(msg)
);
await Promise.all(promises);
}
} finally {
this.clients.delete(ws);
}
}
}
Summary
- Detect upgrades by checking the
Upgrade: websocketheader in incoming requests - Create the WebSocket by returning a 101 response with
webSocket: trueand accessing the.webSocketproperty fromrespondWith() - Activate the socket by calling
ws.accept()before any I/O operations - Use
couple()to proxy traffic between two WebSockets efficiently - Enable hibernation with
ws.accept({ hibernatable: true })for long-lived Durable Object connections - Reference key files:
src/workerd/api/web-socket.c++for the state machine,src/workerd/api/http.c++for upgrade handling, andsrc/workerd/io/hibernation-manager.c++for persistence logic
Frequently Asked Questions
How do I detect a WebSocket upgrade request in workerd?
Check the Upgrade header for the value websocket. While the internal C++ API provides headers.isWebSocket(), the JavaScript environment requires manual inspection: request.headers.get('Upgrade')?.toLowerCase() === 'websocket'. This detection happens before calling Response.acceptWebSocket() in src/workerd/api/http.c++.
What is the difference between ws.accept() and Response.acceptWebSocket()?
Response.acceptWebSocket() (called implicitly when creating a 101 response with webSocket: true) returns the WebSocket object and performs the HTTP upgrade handshake. ws.accept() is called on that socket instance to transition the internal state machine from AwaitingConnection to Accepted, enabling send() and receive() operations. The first handles the protocol layer; the second handles the application state.
Can I implement a WebSocket proxy server in workerd?
Yes. Accept the client WebSocket with Response.acceptWebSocket(), create a backend WebSocket via fetch() to another WebSocket endpoint, then call clientWs.couple(backendWs, request). This method, implemented in src/workerd/api/web-socket.c++, sets up bidirectional pumps that forward frames between the sockets until either side closes.
How does WebSocket hibernation work in workerd?
Hibernation allows Durable Objects to be evicted from memory while keeping WebSocket connections alive. Pass { hibernatable: true } to ws.accept(), then register the socket with state.acceptWebSocket(). The runtime stores the connection in HibernationManagerImpl (defined in src/workerd/io/hibernation-manager.c++) and restores the object when messages arrive, enabling scalable long-running connections without resident memory costs.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →