Skip to main content
The WebSocket API enables real-time, bidirectional communication between clients and servers. Workerd extends the standard WebSocket API with WebSocketPair for creating connected socket pairs.

Overview

Workerd provides:
  • Standard WebSocket interface for client connections
  • WebSocketPair for creating connected socket pairs
  • CloseEvent for handling connection closure
  • Hibernation support for Durable Objects
Implementation: src/workerd/api/web-socket.h and web-socket.c++

WebSocketPair

Create a pair of connected WebSockets:
export default {
  async fetch(request) {
    // Create a WebSocket pair
    const pair = new WebSocketPair();
    
    // Accept the server side
    pair[1].accept();
    
    // Set up event handlers
    pair[1].addEventListener('message', event => {
      console.log('Received:', event.data);
      pair[1].send('Echo: ' + event.data);
    });
    
    // Return the client side to the caller
    return new Response(null, {
      status: 101,
      webSocket: pair[0]
    });
  }
};
Source: src/workerd/api/tests/websocket-test.js

Accepting WebSocket connections

Handle the WebSocket upgrade:
export default {
  async fetch(request) {
    // Check if it's a WebSocket upgrade request
    if (request.headers.get('Upgrade') !== 'websocket') {
      return new Response('Expected WebSocket', { status: 400 });
    }
    
    const pair = new WebSocketPair();
    const [client, server] = pair;
    
    // Accept the connection
    server.accept();
    
    // Handle messages
    server.addEventListener('message', event => {
      server.send(`You said: ${event.data}`);
    });
    
    // Handle close
    server.addEventListener('close', event => {
      console.log('Connection closed:', event.code, event.reason);
    });
    
    // Return the client side
    return new Response(null, {
      status: 101,
      webSocket: client
    });
  }
};

Sending messages

Text messages

webSocket.send('Hello World');

Binary messages

// ArrayBuffer
const buffer = new Uint8Array([1, 2, 3, 4]).buffer;
webSocket.send(buffer);

// Typed array
const bytes = new Uint8Array([1, 2, 3, 4]);
webSocket.send(bytes);

JSON messages

const data = { type: 'greeting', message: 'Hello' };
webSocket.send(JSON.stringify(data));

Receiving messages

Listen for messages with event listeners:
webSocket.addEventListener('message', event => {
  // event.data contains the message (string or ArrayBuffer)
  if (typeof event.data === 'string') {
    console.log('Text message:', event.data);
  } else {
    console.log('Binary message:', new Uint8Array(event.data));
  }
});

Parsing JSON messages

webSocket.addEventListener('message', event => {
  try {
    const data = JSON.parse(event.data);
    console.log('Received:', data);
  } catch (error) {
    console.error('Invalid JSON:', error);
  }
});

Closing connections

Graceful close

// Close with default code (1000)
webSocket.close();

// Close with custom code and reason
webSocket.close(1000, 'Normal closure');

Close event

Handle connection closure:
webSocket.addEventListener('close', event => {
  console.log('Closed:', event.code, event.reason);
  console.log('Was clean:', event.wasClean);
});

Close codes

Common WebSocket close codes:
  • 1000 - Normal closure
  • 1001 - Going away (e.g., server shutdown)
  • 1002 - Protocol error
  • 1003 - Unsupported data
  • 1006 - Abnormal closure (no close frame)
  • 1008 - Policy violation
  • 1009 - Message too big
  • 1011 - Internal server error

Error handling

Handle errors with event listeners:
webSocket.addEventListener('error', event => {
  console.error('WebSocket error:', event);
});

// Errors automatically close the connection
webSocket.addEventListener('close', event => {
  if (!event.wasClean) {
    console.error('Connection closed abnormally');
  }
});

Durable Objects chat example

Complete example from src/workerd/api/samples/durable-objects-chat/chat.js:
export class ChatRoom {
  constructor(state, env) {
    this.state = state;
    this.sessions = [];
  }

  async fetch(request) {
    if (request.headers.get('Upgrade') !== 'websocket') {
      return new Response('Expected WebSocket', { status: 400 });
    }

    const pair = new WebSocketPair();
    await this.handleSession(pair[1]);

    return new Response(null, {
      status: 101,
      webSocket: pair[0]
    });
  }

  async handleSession(webSocket) {
    webSocket.accept();

    // Add to sessions list
    const session = { webSocket };
    this.sessions.push(session);

    // Handle messages
    webSocket.addEventListener('message', async event => {
      const data = JSON.parse(event.data);
      
      // Broadcast to all sessions
      this.broadcast(JSON.stringify({
        name: session.name,
        message: data.message
      }));
      
      // Save to storage
      const key = new Date().toISOString();
      await this.state.storage.put(key, event.data);
    });

    // Handle close
    webSocket.addEventListener('close', () => {
      this.sessions = this.sessions.filter(s => s !== session);
      this.broadcast(JSON.stringify({ quit: session.name }));
    });
  }

  broadcast(message) {
    for (const session of this.sessions) {
      try {
        session.webSocket.send(message);
      } catch (error) {
        // Connection already closed
      }
    }
  }
}
Source: src/workerd/api/samples/durable-objects-chat/chat.js:213

Hibernatable WebSockets

Durable Objects can use hibernatable WebSockets to reduce memory usage:
export class HibernatableRoom {
  constructor(state, env) {
    this.state = state;
    this.state.setHibernatableWebSocketEventTimeout(30_000);
  }

  async webSocketMessage(ws, message) {
    // Called when a message is received
    // The Durable Object may have been hibernating
    this.broadcast(message);
  }

  async webSocketClose(ws, code, reason, wasClean) {
    // Called when a connection closes
    console.log('Connection closed:', code, reason);
  }

  async webSocketError(ws, error) {
    // Called when an error occurs
    console.error('WebSocket error:', error);
  }
}
Implementation: src/workerd/api/hibernatable-web-socket.h

Best practices

The server-side WebSocket must call accept() before sending or receiving messages:
const pair = new WebSocketPair();
pair[1].accept(); // Required!
pair[1].send('Hello');
Always add error and close event listeners:
webSocket.addEventListener('error', event => {
  console.error('Error:', event);
});

webSocket.addEventListener('close', event => {
  if (!event.wasClean) {
    console.error('Abnormal closure');
  }
});
Always validate and sanitize incoming messages:
webSocket.addEventListener('message', event => {
  try {
    const data = JSON.parse(event.data);
    
    // Validate
    if (typeof data.message !== 'string') {
      throw new Error('Invalid message format');
    }
    
    // Sanitize
    const sanitized = data.message.slice(0, 1000);
    
    // Process
    processMessage(sanitized);
  } catch (error) {
    webSocket.close(1003, 'Invalid message');
  }
});
Protect against message spam:
let messageCount = 0;
let resetTime = Date.now() + 60000;

webSocket.addEventListener('message', event => {
  if (Date.now() > resetTime) {
    messageCount = 0;
    resetTime = Date.now() + 60000;
  }
  
  if (++messageCount > 100) {
    webSocket.close(1008, 'Rate limit exceeded');
    return;
  }
  
  // Process message
});

CloseEvent

The CloseEvent interface provides information about connection closure:
webSocket.addEventListener('close', event => {
  console.log('Code:', event.code);       // Close code
  console.log('Reason:', event.reason);   // Close reason string
  console.log('Was clean:', event.wasClean); // true if clean close
});
Definition: src/workerd/api/web-socket.h:32

Implementation details

The WebSocket API is implemented in:
  • src/workerd/api/web-socket.h / .c++ - Main WebSocket implementation (1700+ lines)
  • src/workerd/api/hibernatable-web-socket.h / .c++ - Hibernation support for Durable Objects
WebSocket pairs use the standard socketpair concept where two connected sockets communicate with each other.

Build docs developers (and LLMs) love