Tunnels
The sandbox.tunnels namespace exposes a service running inside a sandbox on a *.trycloudflare.com URL. The SDK runs cloudflared inside the container and opens a persistent QUIC connection to Cloudflare's edge; no Cloudflare account, DNS record, or custom domain is required.
- RPC transport. Calling
sandbox.tunnelson HTTP/Websocket transports will throw"RPC transport required". See Transport configuration. - glibc image variant. The default,
python,opencode, anddesktopimages shipcloudflared. Themusl/Alpine variant does not — there is no upstreamcloudflaredbuild for musl at this time.
Return a tunnel record for port. The SDK spawns a fresh cloudflared process inside the container if not already running. The method is idempotent so repeated calls for the same port return the same record.
const tunnel = await sandbox.tunnels.get(port: number): Promise<TunnelInfo>Parameters:
port— Port number inside the sandbox to expose (1024-65535, excluding reserved ports). The service to tunnel to must already be listening on0.0.0.0:<port>inside the container.
Returns: Promise<TunnelInfo> — the tunnel record. See TunnelInfo.
import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default { async fetch(request, env) { const sandbox = getSandbox(env.Sandbox, "my-sandbox");
await sandbox.startProcess("python -m http.server 8080");
const tunnel = await sandbox.tunnels.get(8080); console.log(tunnel.url); // → https://random-words-here.trycloudflare.com
// Repeated calls for the same port return the same record. const same = await sandbox.tunnels.get(8080); console.log(same.url === tunnel.url); // true
return Response.json({ url: tunnel.url }); },};import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default { async fetch(request: Request, env: Env): Promise<Response> { const sandbox = getSandbox(env.Sandbox, "my-sandbox");
await sandbox.startProcess("python -m http.server 8080");
const tunnel = await sandbox.tunnels.get(8080); console.log(tunnel.url); // → https://random-words-here.trycloudflare.com
// Repeated calls for the same port return the same record. const same = await sandbox.tunnels.get(8080); console.log(same.url === tunnel.url); // true
return Response.json({ url: tunnel.url });
},};Return every tunnel currently tracked for this sandbox.
const tunnels = await sandbox.tunnels.list(): Promise<TunnelInfo[]>Returns: Promise<TunnelInfo[]> — an array of TunnelInfo records. Empty when no tunnels are active.
const tunnels = await sandbox.tunnels.list();
for (const tunnel of tunnels) { console.log(`port ${tunnel.port} → ${tunnel.url}`);}const tunnels = await sandbox.tunnels.list();
for (const tunnel of tunnels) {console.log(`port ${tunnel.port} → ${tunnel.url}`);}Tear down a tunnel. Accepts either the port number or the TunnelInfo record returned by get(). Idempotent — destroying an unknown port resolves successfully.
await sandbox.tunnels.destroy(portOrInfo: number | TunnelInfo): Promise<void>Parameters:
portOrInfo— Either the port number or theTunnelInforecord returned byget().
const tunnel = await sandbox.tunnels.get(8080);
// Tear down by port number...await sandbox.tunnels.destroy(8080);
// ...or by the record.await sandbox.tunnels.destroy(tunnel);const tunnel = await sandbox.tunnels.get(8080);
// Tear down by port number...await sandbox.tunnels.destroy(8080);
// ...or by the record.await sandbox.tunnels.destroy(tunnel);| Field | Type | Description |
|---|---|---|
id | string | SDK-assigned identifier for the tunnel (for example, quick-9f2c8a1d). |
port | number | Port number inside the sandbox that the tunnel proxies to. |
url | string | Public URL — https://<random-words>.trycloudflare.com. |
hostname | string | Hostname component of url (<random-words>.trycloudflare.com). |
createdAt | string | ISO-8601 timestamp of when the tunnel was created. |
interface TunnelInfo { id: string; port: number; url: string; hostname: string; createdAt: string;}- URLs do not survive container restart. Cloudflare assigns the hostname during
cloudflared's startup handshake, so every restart yields a new URL. The SDK clears its tunnel cache when the container starts, so the nexttunnels.get(port)returns a fresh record. - No uptime guarantee. Cloudflare positions
trycloudflare.comas a debug aid, not a production target. UseexposePort()with a custom domain for production. - No Server-Sent Events. The
trycloudflare.comedge bufferstext/event-streamresponses, so SSE does not reach the client. WebSockets work normally. - No persistent hostname. Every restart picks a new
<random-words>.trycloudflare.com. If you need a stable URL, useexposePort()with a custom token. - Brief DNS warm-up. The first request through a brand-new URL can take a couple of seconds while DNS propagates, even after
get()resolves. - WARP / Zero Trust egress. If your local machine runs Cloudflare WARP or another Zero Trust egress policy, outbound traffic to
api.trycloudflare.comand the cloudflared edge can be blocked. When that happens,tunnels.get()hangs on the edge handshake and eventually times out. Disable WARP or add an egress exception for these destinations. - No musl/Alpine support. The musl image variant does not include
cloudflared. Use one of the glibc-based image variants (default,python,opencode,desktop).
- Preview URLs concept — Worker-fronted preview URLs and how they differ from quick tunnels.
- Ports API —
exposePort()and the Worker-fronted preview URL flow. - Expose services guide — End-to-end walkthrough for exposing services in production.
- Transport configuration — RPC vs. route-based transport.