Tunnels·续
| Variable | Required? | Notes |
|---|---|---|
CLOUDFLARE_API_TOKEN |
Yes | Store as a secret with wrangler secret put. |
CLOUDFLARE_ACCOUNT_ID |
Only if the token sees multiple accounts | Inferred from the token otherwise. |
CLOUDFLARE_ZONE_ID |
Only if the token sees multiple zones | Inferred from the token otherwise. |
npx wrangler secret put CLOUDFLARE_API_TOKEN
For local development, place the variables in .dev.vars (gitignored). For production, set the non-secret IDs (when needed) under vars in your Wrangler config:
{
"vars": {
"CLOUDFLARE_ACCOUNT_ID": "<account-id>",
"CLOUDFLARE_ZONE_ID": "<zone-id>"
}
}
When inference fails, the SDK throws a clear error naming the variable to set.
Example
import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(request, env) {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
// Reuse an existing app process across container restarts, or start it.
let proc = await sandbox.getProcess("app");
if (!proc) {
try {
proc = await sandbox.startProcess("python -m http.server 8080", {
processId: "app",
});
} catch (err) {
if (err?.code !== "PROCESS_ALREADY_EXISTS") throw err;
proc = await sandbox.getProcess("app");
}
}
// Provision (or reuse) https://app.example.com pointing at port 8080.
const tunnel = await sandbox.tunnels.get(8080, { name: "app" });
console.log(tunnel.url); // → https://app.example.com
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");
// Reuse an existing app process across container restarts, or start it.
let proc = await sandbox.getProcess('app');
if (!proc) {
try {
proc = await sandbox.startProcess('python -m http.server 8080', { processId: 'app' });
} catch (err) {
if ((err as { code?: string })?.code !== 'PROCESS_ALREADY_EXISTS') throw err;
proc = await sandbox.getProcess('app');
}
}
// Provision (or reuse) https://app.example.com pointing at port 8080.
const tunnel = await sandbox.tunnels.get(8080, { name: "app" });
console.log(tunnel.url); // → https://app.example.com
return Response.json({ url: tunnel.url });
},
};
`name` must be a single DNS label
name must match ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ and be 1–63 characters — no dots, no uppercase, no leading/trailing hyphens. This restriction exists because Cloudflare Universal SSL only issues certificates for <label>.<zone>. Multi-label hostnames need Advanced Certificate Manager or a delegated subdomain zone, which are out of scope for sandbox.tunnels.
Lifecycle
Named tunnels are designed to outlive the container that provisioned them:
- First call to
sandbox.tunnels.get(port, { name }): - Resolves
<name>.<your-zone>from the configured zone ID. - Creates a Cloudflare Tunnel resource named
sandbox-<sandbox-id>-<name>tagged with the sandbox ID. - Upserts a proxied
CNAMEfrom<name>.<your-zone>to<tunnel-id>.cfargotunnel.com. - Spawns
cloudflaredinside the container with the tunnel's token. - Subsequent calls with the same
(port, name)return the cached record without contacting Cloudflare. - Container restart (Durable Object eviction, deploy, crash):
cloudflareddies with the container, but the Cloudflare Tunnel and DNS record are preserved.- On the next
get(port, { name }), the SDK rediscovers the tagged tunnel via the Cloudflare API and respawnscloudflared. The hostname is unchanged. - Explicit teardown with
sandbox.tunnels.destroy(port): - Stops
cloudflaredinside the container. - Deletes the Cloudflare Tunnel resource.
- Deletes the proxied
CNAMErecord. - Sandbox destroy with
sandbox.destroy()tears down every tunnel the sandbox provisioned, including the Cloudflare-side resources, before stopping the container.
If destroy() fails to reach the Cloudflare API (for example, the token was revoked between get() and destroy()), the SDK logs a warning naming the orphaned tunnelId and dnsRecordId so you can clean up manually from the dashboard.
Cloudflare resources and tagging
Named tunnels create resources on your Cloudflare account, outside the sandbox container. They are not stored in Durable Object storage and do not count against sandbox quotas, but they do show up in the Cloudflare dashboard and consume your account's tunnel and DNS quotas.
For each (sandbox, name) pair, the SDK creates:
| Resource | Name / location | Identifier |
|---|---|---|
| Cloudflare Tunnel | Networking > Tunnels | sandbox-<sandbox-id>-<name> |
| Proxied DNS record | Your zone, DNS > Records | CNAME <name>.<zone> → <tunnel-id>.cfargotunnel.com |
Both resources are tagged so you can audit, query, and bulk-clean them from the dashboard or API:
- Tunnel metadata:
{ sandboxId, createdBy: 'sandbox-sdk', name, port } - DNS record comment:
sandbox-<sandbox-id> - Resource tag (Enterprise plans only):
sandboxId:<sandbox-id>
On non-Enterprise plans, Cloudflare rejects resource tags; the SDK detects this and retries the request without tags. The DNS comment and tunnel metadata still apply, so you can always trace a resource back to its sandbox.
To list every tunnel created by the SDK for a given account:
curl "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/cfd_tunnel?name=sandbox-" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
Limitations
Both tunnel flavors:
- 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. - 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.
Quick tunnels only:
- 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. Use a named tunnel for a stable hostname. - No uptime guarantee. Cloudflare positions
trycloudflare.comas a debug aid, not a production target. - No Server-Sent Events. The
trycloudflare.comedge bufferstext/event-streamresponses, so SSE events never reach the client. WebSockets work normally. Use a named tunnel if your service streams SSE.
Named tunnels only:
- Single DNS label.
namemust not contain dots. Universal SSL only covers<name>.<your-zone>. - Counts against your zone's quotas. Each named tunnel creates a Cloudflare Tunnel and a DNS record on your account. See Cloudflare Tunnel limits.
- Cleanup requires the API token. If
destroy()runs after the token has been revoked, the Cloudflare-side resources are orphaned. The SDK logs the orphan IDs so you can remove them manually.
Related resources
- 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.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/sdk/api/tunnels/#page","headline":"Tunnels","description":"Expose Sandbox SDK 0.x services on the public Internet with quick tunnels (*.trycloudflare.com) or named tunnels bound to a hostname on your Cloudflare zone.","url":"https://developers.cloudflare.com/sandbox/sdk/api/tunnels/","inLanguage":"en","image":"https://developers.cloudflare.com/sandbox/sdk/api/tunnels/og.png?v=5d03fc562db367fd","dateModified":"2026-09-30","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: Deploy the Sandbox SDK 0.x bridge Worker to control sandboxes over HTTP from any language or platform. title: Sandbox bridge image: https://developers.cloudflare.com/sandbox/sdk/bridge/og.png?v=5eeb7f4b8d274a6a
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