Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cloudflare/sandbox-sdk/llms.txt
Use this file to discover all available pages before exploring further.
Overview
WebSocket transport multiplexes multiple HTTP-like requests over a single WebSocket connection, eliminating sub-request limits when using the SDK inside Cloudflare Workers or Durable Objects.
Why WebSocket transport?
Cloudflare Workers and Durable Objects have sub-request limits:
- Workers: 1,000 sub-requests per request
- Durable Objects: 1,000 sub-requests per Durable Object instance
Each SDK operation (exec, file read, process management) typically makes 1-3 HTTP requests. Complex workflows can quickly exhaust these limits.
WebSocket transport solves this by:
- Using only 1 sub-request for the WebSocket upgrade
- Multiplexing unlimited operations over that single connection
- Maintaining request/response semantics for easy migration
Enabling WebSocket transport
Basic setup
import { getSandbox } from '@cloudflare/sandbox';
export default {
async fetch(request: Request, env: Env) {
const sandbox = getSandbox(env.SANDBOX, 'my-sandbox', {
useWebSocket: true // Enable WebSocket transport
});
// All operations now use WebSocket
await sandbox.exec('npm install');
await sandbox.exec('npm test');
return new Response('OK');
}
};
Inside a Durable Object
import { Sandbox, getSandbox } from '@cloudflare/sandbox';
export class MyDurableObject extends DurableObject {
async fetch(request: Request) {
const sandbox = getSandbox(this.env.SANDBOX, 'do-sandbox', {
useWebSocket: true
});
// Run many operations without hitting sub-request limits
for (let i = 0; i < 100; i++) {
await sandbox.exec(`echo "Operation ${i}"`);
}
return new Response('Completed 100 operations');
}
}
How it works
Connection lifecycle
- Upgrade request: SDK sends HTTP upgrade request to container
- WebSocket established: Container accepts and maintains connection
- Request multiplexing: SDK sends JSON-encoded requests with unique IDs
- Response matching: Container sends responses with matching IDs
- Cleanup: Connection closes when sandbox is destroyed or explicitly disconnected
Request/response protocol
Requests and responses use a simple JSON protocol:
Request format:
{
type: 'request',
id: 'req-abc123', // Unique request ID
method: 'POST', // HTTP method
path: '/exec', // API endpoint
body: { command: 'ls' } // Request payload
}
Response format:
{
type: 'response',
id: 'req-abc123', // Matches request ID
status: 200, // HTTP status
body: { ... }, // Response payload
done: true // Final response for this request
}
Stream chunk format:
{
type: 'stream',
id: 'req-abc123',
event: 'log', // Optional event type
data: '{"output": "line"}' // SSE-format data
}
Concurrent requests
Multiple requests can be in-flight simultaneously:
// All three requests share the same WebSocket connection
const [result1, result2, result3] = await Promise.all([
sandbox.exec('npm install'),
sandbox.exec('npm test'),
sandbox.exec('npm run build')
]);
Each request gets a unique ID and responses are matched accordingly.
Connection management
Automatic connection
Connections are established automatically on first use:
const sandbox = getSandbox(env.SANDBOX, 'my-sandbox', {
useWebSocket: true
});
// Connection established here (on first operation)
await sandbox.exec('echo "Hello"');
// Reuses existing connection
await sandbox.exec('echo "World"');
Explicit connection control
For advanced use cases, manually control connections:
const sandbox = getSandbox(env.SANDBOX, 'my-sandbox', {
useWebSocket: true
});
// Establish connection early
await sandbox.client.transport.connect();
// Perform operations
await sandbox.exec('command1');
await sandbox.exec('command2');
// Disconnect when done
sandbox.client.transport.disconnect();
Connection sharing
Multiple operations automatically share the same connection:
// Only ONE WebSocket connection is used
const sandbox = getSandbox(env.SANDBOX, 'my-sandbox', {
useWebSocket: true
});
// All operations share the connection
await sandbox.readFile('/file1.txt');
await sandbox.writeFile('/file2.txt', 'data');
await sandbox.exec('ls');
const processes = await sandbox.listProcesses();
Streaming over WebSocket
Streaming operations work seamlessly:
const sandbox = getSandbox(env.SANDBOX, 'my-sandbox', {
useWebSocket: true
});
const process = await sandbox.startProcess({
command: 'npm run dev'
});
// Stream logs over WebSocket
for await (const log of process.logs()) {
console.log(log.output);
}
The WebSocket transport converts stream chunks to SSE format for compatibility with existing code.
Error handling
Connection errors
Handle connection failures gracefully:
try {
const sandbox = getSandbox(env.SANDBOX, 'my-sandbox', {
useWebSocket: true,
containerTimeouts: {
connect: 30000 // 30 second connection timeout
}
});
await sandbox.exec('echo "test"');
} catch (error) {
if (error.message.includes('WebSocket')) {
console.error('Failed to establish WebSocket connection');
// Fallback to HTTP or retry
}
}
Request timeouts
Configure request-level timeouts:
const sandbox = getSandbox(env.SANDBOX, 'my-sandbox', {
useWebSocket: true,
containerTimeouts: {
request: 120000 // 2 minute request timeout
}
});
try {
await sandbox.exec('long-running-command');
} catch (error) {
if (error.message.includes('timeout')) {
console.error('Request timed out');
}
}
Connection loss
When the WebSocket closes unexpectedly, pending requests are rejected:
const sandbox = getSandbox(env.SANDBOX, 'my-sandbox', {
useWebSocket: true
});
try {
// Long-running operation
await sandbox.exec('sleep 300');
} catch (error) {
if (error.message.includes('WebSocket closed')) {
console.error('Connection lost during operation');
// Reconnect and retry
}
}
Latency
WebSocket transport has similar latency to HTTP for individual requests:
- First request: +10-30ms (connection establishment)
- Subsequent requests: Similar to HTTP (no additional overhead)
Throughput
WebSocket excels with many small requests:
// HTTP: 100 sub-requests
for (let i = 0; i < 100; i++) {
await sandbox.exec(`echo ${i}`);
}
// WebSocket: 1 sub-request (upgrade) + 100 multiplexed operations
const sandbox = getSandbox(env.SANDBOX, 'my-sandbox', {
useWebSocket: true
});
for (let i = 0; i < 100; i++) {
await sandbox.exec(`echo ${i}`);
}
Memory usage
WebSocket maintains a connection with pending request tracking:
- Per connection: ~2-5 KB base overhead
- Per pending request: ~200 bytes
- Stream buffers: ~16 KB per active stream
Comparison with HTTP transport
| Feature | HTTP Transport | WebSocket Transport |
|---|
| Sub-requests used | 1-3 per operation | 1 total (upgrade) |
| Connection overhead | None | Initial upgrade (~20ms) |
| Concurrent requests | Supported | Supported |
| Streaming | Server-Sent Events | WebSocket messages |
| Best for | Simple workflows | Complex workflows, DO context |
| Memory | Lower | Slightly higher |
When to use WebSocket transport
✅ Use WebSocket when:
- Running inside a Durable Object with many operations
- Executing complex workflows with 50+ SDK calls
- Approaching Worker sub-request limits
- Long-running processes with streaming output
- Batch operations across multiple files/commands
❌ Use HTTP when:
- Simple one-off operations (1-10 SDK calls)
- Running outside Workers (Node.js, browser)
- Debugging (easier to inspect HTTP traffic)
- Maximum compatibility (no WebSocket support needed)
Configuration options
Transport-specific options
const sandbox = getSandbox(env.SANDBOX, 'my-sandbox', {
useWebSocket: true,
containerTimeouts: {
connect: 30000, // WebSocket connection timeout (ms)
request: 120000 // Individual request timeout (ms)
}
});
Fallback to HTTP
Implement fallback logic for maximum reliability:
async function createSandbox(env: Env, id: string) {
try {
// Try WebSocket first
return getSandbox(env.SANDBOX, id, { useWebSocket: true });
} catch (error) {
console.warn('WebSocket failed, falling back to HTTP');
// Fallback to HTTP
return getSandbox(env.SANDBOX, id, { useWebSocket: false });
}
}
Advanced patterns
Connection pooling
Reuse connections across multiple operations:
class SandboxPool {
private sandbox: Sandbox;
constructor(env: Env) {
this.sandbox = getSandbox(env.SANDBOX, 'pooled', {
useWebSocket: true
});
}
async execute(command: string) {
// All operations share one WebSocket connection
return await this.sandbox.exec(command);
}
disconnect() {
this.sandbox.client.transport.disconnect();
}
}
Request prioritization
Implement priority queues over a single connection:
class PriorityQueue {
private high: Array<() => Promise<any>> = [];
private low: Array<() => Promise<any>> = [];
async process(sandbox: Sandbox) {
// Process high priority first
while (this.high.length > 0) {
const task = this.high.shift()!;
await task();
}
// Then low priority
while (this.low.length > 0) {
const task = this.low.shift()!;
await task();
}
}
addHigh(task: () => Promise<any>) {
this.high.push(task);
}
addLow(task: () => Promise<any>) {
this.low.push(task);
}
}
Batch operations
Execute many operations efficiently:
const sandbox = getSandbox(env.SANDBOX, 'batch', {
useWebSocket: true
});
const files = ['file1.txt', 'file2.txt', 'file3.txt', /* ... 100 files */];
// Process all files over one WebSocket connection
const results = await Promise.all(
files.map(file => sandbox.readFile(file))
);
Debugging
Enable debug logging
import { createLogger } from '@repo/shared';
const logger = createLogger({
level: 'debug',
component: 'websocket'
});
const sandbox = getSandbox(env.SANDBOX, 'debug', {
useWebSocket: true,
logger: logger
});
// Logs all WebSocket messages
await sandbox.exec('echo "test"');
Monitor connection state
const transport = sandbox.client.transport;
if (transport.isConnected()) {
console.log('WebSocket connected');
} else {
console.log('WebSocket disconnected');
}
Inspect message flow
Log request/response pairs for debugging:
class DebugTransport {
async fetch(path: string, options?: RequestInit) {
console.log('→ Request:', { path, options });
const response = await this.transport.fetch(path, options);
console.log('← Response:', { status: response.status });
return response;
}
}
Troubleshooting
WebSocket upgrade fails
Verify the container supports WebSocket:
try {
const sandbox = getSandbox(env.SANDBOX, 'test', {
useWebSocket: true
});
await sandbox.exec('echo "test"');
} catch (error) {
console.error('Upgrade failed:', error.message);
// Fall back to HTTP
}
Messages out of order
Responses may arrive out of order, but they’re matched by ID:
// Request 1 may complete after Request 2
const [r1, r2] = await Promise.all([
sandbox.exec('sleep 5 && echo "first"'), // Slow
sandbox.exec('echo "second"') // Fast
]);
console.log(r1.stdout); // "first" (even though it finished last)
console.log(r2.stdout); // "second"
Connection drops under load
Increase timeouts for high-concurrency scenarios:
const sandbox = getSandbox(env.SANDBOX, 'heavy-load', {
useWebSocket: true,
containerTimeouts: {
connect: 60000, // 1 minute
request: 300000 // 5 minutes
}
});
Memory leaks with streaming
Always consume or cancel streams:
const process = await sandbox.startProcess({ command: 'npm run dev' });
try {
for await (const log of process.logs()) {
console.log(log.output);
if (shouldStop) break;
}
} finally {
// Clean up the stream
await sandbox.killProcess(process.id);
}
Implementation details
For contributors and advanced users:
Transport abstraction
Both HTTP and WebSocket transports implement ITransport:
packages/sandbox/src/clients/transport/types.ts
interface ITransport {
fetch(path: string, options?: RequestInit): Promise<Response>;
fetchStream(path: string, body?: unknown, method?: 'GET' | 'POST'): Promise<ReadableStream<Uint8Array>>;
getMode(): TransportMode;
connect(): Promise<void>;
disconnect(): void;
isConnected(): boolean;
}
Connection establishment
WebSocket upgrade uses different mechanisms based on context:
- Inside DO: Uses
stub.fetch() with upgrade headers
- Browser/Node: Uses standard
new WebSocket(url)
Message encoding
All messages are JSON-encoded:
// SDK → Container
webSocket.send(JSON.stringify({
type: 'request',
id: generateRequestId(),
method: 'POST',
path: '/exec',
body: { command: 'ls' }
}));
// Container → SDK
webSocket.send(JSON.stringify({
type: 'response',
id: 'req-abc123',
status: 200,
body: { stdout: 'file.txt\n', stderr: '', exitCode: 0 },
done: true
}));
Pending request tracking
The SDK maintains a map of pending requests:
packages/sandbox/src/clients/transport/ws-transport.ts:40
private pendingRequests: Map<string, PendingRequest> = new Map();
Each request stores its resolve/reject functions and optional stream controller.