WebSocket connections

Last updated Oct 23, 2026 Note

This page documents Sandbox SDK 0.x for existing applications. For new applications, refer to Sandboxes. To move an existing application to @cloudflare/sandbox 1.0, refer to Migrate from Sandbox SDK 0.x.

This guide shows you how to work with WebSocket servers running in your sandboxes.

Choose your approach

Expose via preview URL - Get a public URL for external clients to connect to. Best for public chat rooms, multiplayer games, or real-time dashboards.

Connect with wsConnect() - Your Worker establishes the WebSocket connection. Best for custom routing logic, authentication gates, or when your Worker needs real-time data from sandbox services.

Connect to WebSocket echo server

Create the echo server:

echo-server.tstypescript

Bun.serve({
    port: 8080,
    hostname: "0.0.0.0",
    fetch(req, server) {
        if (server.upgrade(req)) {
            return;
        }
        return new Response("WebSocket echo server");
    },
    websocket: {
        message(ws, message) {
            ws.send(`Echo: ${message}`);
        },
        open(ws) {
            console.log("Client connected");
        },
        close(ws) {
            console.log("Client disconnected");
        },
    },
});

console.log("WebSocket server listening on port 8080");

Extend the Dockerfile:

Dockerfiledockerfile

FROM docker.io/cloudflare/sandbox:0.12.14

# Copy echo server into the container
COPY echo-server.ts /workspace/echo-server.ts

# Create custom startup script
COPY startup.sh /container-server/startup.sh
RUN chmod +x /container-server/startup.sh

Create startup script:

startup.shbash

#!/bin/bash
# Start your WebSocket server in the background
bun /workspace/echo-server.ts &
# Start SDK's control plane (needed for the SDK to work)
exec bun dist/index.js

Connect from your Worker:

import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
    async fetch(request, env) {
        if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") {
            const sandbox = getSandbox(env.Sandbox, "echo-service");
            return await sandbox.wsConnect(request, 8080);
        }

        return new Response("WebSocket endpoint");
    },
};
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from "@cloudflare/sandbox";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {
      const sandbox = getSandbox(env.Sandbox, 'echo-service');
      return await sandbox.wsConnect(request, 8080);
    }

    return new Response('WebSocket endpoint');

}
};

Client connects:

const ws = new WebSocket('wss://your-worker.com');
ws.onmessage = (event) => console.log(event.data);
ws.send('Hello!'); // Receives: "Echo: Hello!"

Expose WebSocket service via preview URL

Get a public URL for your WebSocket server:

import { getSandbox, proxyToSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
    async fetch(request, env) {
        // Auto-route all requests via proxyToSandbox first
        const proxyResponse = await proxyToSandbox(request, env);
        if (proxyResponse) return proxyResponse;

        // Extract hostname from request
        const { hostname } = new URL(request.url);
        const sandbox = getSandbox(env.Sandbox, "echo-service");

        // Expose the port to get preview URL
        const { url } = await sandbox.exposePort(8080, { hostname });

        // Return URL to clients
        if (request.url.includes("/ws-url")) {
            return Response.json({ url: url.replace("https", "wss") });
        }

        return new Response("Not found", { status: 404 });
    },
};
import { getSandbox, proxyToSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Auto-route all requests via proxyToSandbox first
    const proxyResponse = await proxyToSandbox(request, env);
    if (proxyResponse) return proxyResponse;

    // Extract hostname from request
    const { hostname } = new URL(request.url);
    const sandbox = getSandbox(env.Sandbox, 'echo-service');

    // Expose the port to get preview URL
    const { url } = await sandbox.exposePort(8080, { hostname });

    // Return URL to clients
    if (request.url.includes('/ws-url')) {
      return Response.json({ url: url.replace('https', 'wss') });
    }

    return new Response('Not found', { status: 404 });

}
};

Alternative: quick tunnels

Quick tunnels also handle WebSocket upgrades and do not require a custom domain, so they work on .workers.dev. Swap sandbox.exposePort(8080, { hostname }) for sandbox.tunnels.get(8080) to get a *.trycloudflare.com URL.

Client connects to preview URL:

// Get the preview URL
const response = await fetch('https://your-worker.com/ws-url');
const { url } = await response.json();

// Connect
const ws = new WebSocket(url);
ws.onmessage = (event) => console.log(event.data);
ws.send('Hello!'); // Receives: "Echo: Hello!"

Connect from Worker to get real-time data

Your Worker can connect to a WebSocket service to get real-time data, even when the incoming request isn't a WebSocket:

import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

let initialized = false;

export default {
    async fetch(request, env) {
        // Get or create a sandbox instance
        const sandbox = getSandbox(env.Sandbox, "data-processor");

        // Check for WebSocket upgrade
        const upgrade = request.headers.get("Upgrade")?.toLowerCase();

        if (upgrade === "websocket") {
            // Initialize server on first connection
            if (!initialized) {
                await sandbox.writeFile(
                    "/workspace/server.js",
                    `Bun.serve({
            port: 8080,
            fetch(req, server) {
              server.upgrade(req);
            },
            websocket: {
              message(ws, msg) {
                ws.send(\`Echo: \${msg}\`);
              }
            }
          });`,
                );
                await sandbox.startProcess("bun /workspace/server.js");
                initialized = true;
            }
            // Connect to WebSocket server
            return await sandbox.wsConnect(request, 8080);
        }

        return new Response("Processed real-time data");
    },
};
import { getSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

let initialized = false;

export default {
  async fetch(request: Request, env: Env): Promise<Response> {

     // Get or create a sandbox instance
    const sandbox = getSandbox(env.Sandbox, 'data-processor');

    // Check for WebSocket upgrade
    const upgrade = request.headers.get('Upgrade')?.toLowerCase();

    if (upgrade === 'websocket') {
      // Initialize server on first connection
      if (!initialized) {
        await sandbox.writeFile(
          '/workspace/server.js',
          `Bun.serve({
            port: 8080,
            fetch(req, server) {
              server.upgrade(req);
            },
            websocket: {
              message(ws, msg) {
                ws.send(\`Echo: \${msg}\`);
              }
            }
          });`
        );
        await sandbox.startProcess(
          'bun /workspace/server.js'
        );
        initialized = true;
      }
      // Connect to WebSocket server
      return await sandbox.wsConnect(request, 8080);
    }

    return new Response('Processed real-time data');

}
};

This pattern is useful when you need streaming data from sandbox services but want to return HTTP responses to clients.

Troubleshooting

Upgrade failed

Verify request has WebSocket headers:

console.log(request.headers.get("Upgrade")); // 'websocket'
console.log(request.headers.get("Connection")); // 'Upgrade'
console.log(request.headers.get('Upgrade'));    // 'websocket'
console.log(request.headers.get('Connection')); // 'Upgrade'

Local development

Expose ports in Dockerfile for wrangler dev:

Dockerfiledockerfile

FROM docker.io/cloudflare/sandbox:0.12.14

COPY echo-server.ts /workspace/echo-server.ts
COPY startup.sh /container-server/startup.sh
RUN chmod +x /container-server/startup.sh

# Required for local development
EXPOSE 8080

Note

Port exposure in Dockerfile is only required for local development. In production, all ports are automatically accessible.

Was this helpful?

YesNo

On this page

WebSocket connections - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/sdk/guides/websocket-connections/#page","headline":"WebSocket connections","description":"Connect to WebSocket servers running in Sandbox SDK 0.x sandboxes.","url":"https://developers.cloudflare.com/sandbox/sdk/guides/websocket-connections/","inLanguage":"en","image":"https://developers.cloudflare.com/sandbox/sdk/guides/websocket-connections/og.png?v=940d4f18e858ca31","dateModified":"2026-10-23","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: Access KV, R2, Durable Objects, and other bindings from a Sandbox SDK 0.x sandbox. title: Connect to Workers bindings image: https://developers.cloudflare.com/sandbox/sdk/guides/workers-connections/og.png?v=25b9d14a733d1d23


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt
Use this file to discover all available pages before exploring further.