Skip to content

Workers WebSockets -- Real-Time Connections

DodaTech 7 min read

In this tutorial, you'll learn about Workers WebSockets. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Cloudflare Workers WebSockets provide full-duplex, persistent communication channels between clients and Serverless Workers, enabling real-time features like live chat, streaming data, and collaborative editing without managing dedicated server infrastructure.

Why WebSockets at the Edge

Traditional HTTP follows a request-response pattern where the client must poll for updates. WebSockets invert this model by establishing a long-lived connection that both client and server can push messages through at any time. Cloudflare Workers support WebSocket connections from clients as well as outbound WebSocket connections to origin servers. The key advantage of running WebSocket handling at the edge is that connections terminate at the nearest Cloudflare data center, reducing latency and offloading your origin infrastructure. Unlike Cloudflare's standard HTTP Workers which are stateless and short-lived, WebSocket Workers maintain state for the duration of the connection. For state that must survive across multiple connections, combine WebSockets with REST APIs or Durable Objects.

Real-world use: A stock trading dashboard opens a WebSocket to a Worker that subscribes to price feeds from multiple exchanges. The Worker filters, aggregates, and pushes real-time price updates to the dashboard, all within 50 milliseconds of the market data arriving.

WebSocket Architecture on Workers

flowchart LR
    C[Client Browser] --> WS[WebSocket Upgrade]
    WS --> W[Workers Runtime]
    W --> WS1[WebSocket Connection]
    WS1 --> M[Message Handler]
    M --> P[Push to Client]
    W --> B[Backend Services]
    W --> DO[Durable Objects]

    subgraph Connection_Lifecycle
        U[Upgrade Request] --> A[Accept]
        A --> O[Open]
        O --> MSG[Message Exchange]
        MSG --> CL[Close]
    end

    style WS fill:#f90,color:#fff
    style W fill:#3498db,color:#fff
    style M fill:#2ecc71,color:#fff

A WebSocket connection begins as an HTTP GET request with an Upgrade: <a href="/apis/websocket/">WebSocket</a> header. The Worker handles this request, creates a WebSocket pair, accepts the server-side socket, and returns the client-side socket in a 101 Switching Protocols response.

Basic Echo WebSocket Server

export default {
  async fetch(request) {
    const upgradeHeader = request.headers.get('Upgrade');
    if (!upgradeHeader || upgradeHeader !== 'websocket') {
      return new Response('Expected WebSocket upgrade', { status: 426 });
    }

    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    server.accept();

    server.addEventListener('message', (event) => {
      server.send(`Echo: ${event.data}`);
    });

    server.addEventListener('close', () => {
      console.log('Connection closed');
    });

    server.addEventListener('error', (err) => {
      console.error('WebSocket error:', err.message);
    });

    return new Response(null, {
      status: 101,
      webSocket: client
    });
  }
};

// Expected behavior (using a <a href="/apis/websocket/">Websocket</a> client):
// Send: "Hello" -> Receive: "Echo: Hello"
// Send: "ping" -> Receive: "Echo: ping"
// Close -> Server logs "Connection closed"

The WebSocketPair API creates two connected WebSocket objects. The server socket is used within the Worker; the client socket is returned to the user. Calling server.accept() is required before messages can be received. The Worker can send messages in response to events or on a timer using ctx.waitUntil().

Bidirectional Chat Relay

export default {
  async fetch(request, env, ctx) {
    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    server.accept();

    // Connect to upstream chat service
    const upstreamUrl = 'wss://chat.internal.example.com';
    const upstream = new WebSocket(upstreamUrl);

    await new Promise((resolve, reject) => {
      upstream.addEventListener('open', () => resolve());

      upstream.addEventListener('message', (event) => {
        server.send(event.data);
      });

      upstream.addEventListener('close', () => {
        server.send(JSON.stringify({ type: 'system', text: 'Disconnected from upstream' }));
      });
    });

    server.addEventListener('message', (event) => {
      upstream.send(event.data);
    });

    server.addEventListener('close', () => {
      upstream.close();
    });

    ctx.waitUntil(promise);
    return new Response(null, { status: 101, webSocket: client });
  }
};

// Expected behavior:
// 1. Client connects and is immediately relayed to upstream chat
// 2. Client sends "Hello everyone" -> Upstream receives it
// 3. Upstream sends broadcast -> Client receives it
// 4. Client disconnects -> Upstream connection closes

When connecting to an upstream WebSocket from a Worker, the upstream connection is established asynchronously. Messages flow bidirectionally between the client and the upstream service, with the Worker acting as a transparent relay. This pattern is useful for integrating edge WebSocket clients with existing backend services.

WebSocket with Hibernation for Scale

import { DurableObject } from 'cloudflare:workers';

export class GameSession extends DurableObject {
  constructor(ctx, env) {
    super(ctx, env);
    this.players = new Map();
  }

  async fetch(request) {
    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    this.ctx.acceptWebSocket(server);
    const playerId = crypto.randomUUID();
    this.players.set(server, { id: playerId, score: 0 });

    server.addEventListener('message', (event) => {
      const msg = JSON.parse(event.data);
      if (msg.type === 'move') {
        this.players.get(server).score += msg.points;
        for (const [ws, info] of this.players) {
          if (ws !== server) {
            ws.send(JSON.stringify({
              type: 'score_update',
              playerId: info.id,
              score: info.score
            }));
          }
        }
      }
    });

    server.send(JSON.stringify({
      type: 'connected',
      playerId: playerId,
      activePlayers: this.players.size
    }));

    return new Response(null, { status: 101, webSocket: client });
  }
}

// Expected behavior:
// 1. Two players connect to the same GameSession
// 2. Player A sends {"type": "move", "points": 10}
// 3. Player B receives {"type": "score_update", "playerId": "...", "score": 10}
// 4. The Durable Object hibernates between messages, saving resources

Durable Objects with acceptWebSocket enable the Hibernation API, which suspends the object when no WebSocket messages are being processed. When a message arrives, the object wakes up, processes it, and goes back to sleep. This dramatically reduces costs for idle connections.

Common Errors

Error Cause Fix
<a href="/apis/websocket/">WebSocket</a> handshake failed Missing or invalid upgrade headers Ensure the client sends Upgrade: <a href="/apis/websocket/">WebSocket</a> and Connection: Upgrade headers
Cannot accept <a href="/apis/websocket/">WebSocket</a> in this context WebSocket accepted outside of fetch handler Call server.accept() inside the fetch handler before returning the 101 response
<a href="/apis/websocket/">WebSocket</a> is not connected Message sent after connection closed Check socket.readyState before sending; handle close events to clean up references
<a href="/apis/websocket/">WebSocket</a> connection limit exceeded Too many concurrent connections on free plan Upgrade to a paid Workers plan which allows higher connection limits
Serialization error Trying to send non-string data without encoding Use JSON.stringify() for objects and new TextEncoder() for binary data

Practice Questions

  1. What HTTP status code indicates a successful WebSocket upgrade?
  2. How do you create a WebSocket pair inside a Workers fetch handler?
  3. What advantage does the Hibernation API provide for WebSocket connections?

FAQ

How long can a WebSocket connection stay open on Workers?

WebSocket connections can remain open indefinitely as long as both sides continue to exchange messages. If no messages are sent for a period (typically 2-5 minutes depending on network conditions), the connection may be closed by intermediary proxies. Implement a periodic ping/pong heartbeat to keep connections alive.

{{< faq "Can Workers connect to external Websocket servers?">}} Yes. Workers can initiate outbound WebSocket connections to any WebSocket server using the standard new <a href="/apis/websocket/">WebSocket</a>(url) API. This is useful for proxying, aggregating data from multiple sources, or connecting to backend real-time services. {{< /faq >}}

{{< faq "Are Websocket connections billed like regular Workers?">}} WebSocket connections incur CPU time only when messages are being processed. Idle connections with no message activity do not consume CPU time. However, the connection duration may affect billing if the Worker is using Durable Objects with the Hibernation API.{{< /faq >}}

Summary

Workers WebSockets enable bidirectional real-time communication between clients and Serverless applications at the edge. The WebSocketPair API allows Workers to accept WebSocket upgrades, while outbound WebSocket connections integrate with external services. Combined with Durable Objects and the Hibernation API, WebSocket Workers can scale to thousands of concurrent connections with minimal cost. Use WebSocket Workers for live dashboards, multiplayer games, chat applications, and real-time data streaming. Doda Browser uses edge WebSockets to sync browsing sessions across devices in real time.

This guide is brought to you by the developers of Cloudflare, WebSocket protocol, and Durga Antivirus Pro at DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro