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.
Related resources
- Ports API reference - Complete API documentation
- Preview URLs concept - How preview URLs work
- Tunnels API - Zero-config
*.trycloudflare.comURLs for WebSocket services in development - Background processes guide - Managing long-running services
Was this helpful?
YesNo
On this page
{"@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
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.
Docs