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:

  1. First call to sandbox.tunnels.get(port, { name }):
  2. Resolves <name>.<your-zone> from the configured zone ID.
  3. Creates a Cloudflare Tunnel resource named sandbox-<sandbox-id>-<name> tagged with the sandbox ID.
  4. Upserts a proxied CNAME from <name>.<your-zone> to <tunnel-id>.cfargotunnel.com.
  5. Spawns cloudflared inside the container with the tunnel's token.
  6. Subsequent calls with the same (port, name) return the cached record without contacting Cloudflare.
  7. Container restart (Durable Object eviction, deploy, crash):
  8. cloudflared dies with the container, but the Cloudflare Tunnel and DNS record are preserved.
  9. On the next get(port, { name }), the SDK rediscovers the tagged tunnel via the Cloudflare API and respawns cloudflared. The hostname is unchanged.
  10. Explicit teardown with sandbox.tunnels.destroy(port):
  11. Stops cloudflared inside the container.
  12. Deletes the Cloudflare Tunnel resource.
  13. Deletes the proxied CNAME record.
  14. 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.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.
  • 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 next tunnels.get(port) returns a fresh record. Use a named tunnel for a stable hostname.
  • No uptime guarantee. Cloudflare positions trycloudflare.com as a debug aid, not a production target.
  • No Server-Sent Events. The trycloudflare.com edge buffers text/event-stream responses, 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. name must 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.

Was this helpful?

YesNo

On this page

Tunnels·续 - 图1Docs

{"@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


Skip to content

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.