Skip to content
Docs

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.

Requirements

  • RPC transport. Calling sandbox.tunnels on HTTP/Websocket transports will throw "RPC transport required". See Transport configuration.
  • glibc image variant. The default, python, opencode, and desktop images ship cloudflared. The musl/Alpine variant does not — there is no upstream cloudflared build for musl at this time.

Methods

tunnels.get()

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.

TypeScript
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 on 0.0.0.0:<port> inside the container.

Returns: Promise<TunnelInfo> — the tunnel record. See TunnelInfo.

JavaScript
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 });
},
};

tunnels.list()

Return every tunnel currently tracked for this sandbox.

TypeScript
const tunnels = await sandbox.tunnels.list(): Promise<TunnelInfo[]>

Returns: Promise<TunnelInfo[]> — an array of TunnelInfo records. Empty when no tunnels are active.

JavaScript
const tunnels = await sandbox.tunnels.list();
for (const tunnel of tunnels) {
console.log(`port ${tunnel.port}${tunnel.url}`);
}

tunnels.destroy()

Tear down a tunnel. Accepts either the port number or the TunnelInfo record returned by get(). Idempotent — destroying an unknown port resolves successfully.

TypeScript
await sandbox.tunnels.destroy(portOrInfo: number | TunnelInfo): Promise<void>

Parameters:

  • portOrInfo — Either the port number or the TunnelInfo record returned by get().
JavaScript
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);

Types

TunnelInfo

FieldTypeDescription
idstringSDK-assigned identifier for the tunnel (for example, quick-9f2c8a1d).
portnumberPort number inside the sandbox that the tunnel proxies to.
urlstringPublic URL — https://<random-words>.trycloudflare.com.
hostnamestringHostname component of url (<random-words>.trycloudflare.com).
createdAtstringISO-8601 timestamp of when the tunnel was created.
TypeScript
interface TunnelInfo {
id: string;
port: number;
url: string;
hostname: string;
createdAt: string;
}

Limitations

  • 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 next tunnels.get(port) returns a fresh record.
  • No uptime guarantee. Cloudflare positions trycloudflare.com as a debug aid, not a production target. Use exposePort() with a custom domain for production.
  • No Server-Sent Events. The trycloudflare.com edge buffers text/event-stream responses, 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, use exposePort() 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.com and 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).