# How to Implement WebSocket Servers in workerd: A Complete Guide

> Implement WebSocket servers in workerd. Detect upgrade requests, return 101, accept the WebSocket, and use send/receive for full duplex communication. Get the complete guide.

- Repository: [Cloudflare/workerd](https://github.com/cloudflare/workerd)
- Tags: how-to-guide
- Published: 2026-03-18

---

**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:

1. **Detection** – Identify the `Upgrade: websocket` header in the incoming request
2. **Acceptance** – Return a 101 response via `Response.acceptWebSocket()` to obtain the server-side `WebSocket` object
3. **Activation** – Call `ws.accept()` to transition from `AwaitingConnection` to `Accepted` state 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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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:

```javascript
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: websocket` header in incoming requests
- **Create the WebSocket** by returning a 101 response with `webSocket: true` and accessing the `.webSocket` property from `respondWith()`
- **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, and `src/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.