---
title: Preview URLs
description: Sandbox SDK preview URLs provide public HTTPS access to services running inside sandboxes.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> 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. 

[Skip to content](#%5Ftop) 

# Preview URLs

# Quick deployment

For quick preview deployments we recommend using [Cloudflare Tunnel ↗](https://developers.cloudflare.com/tunnel/) to generate preview URLs to your web services. These work across local development, workers.dev and production usage.

TypeScript

```
await sandbox.startProcess("python -m http.server 8000");const tunnel = await sandbox.tunnels.get(8000);console.log(tunnel.url);// https://acute-llama-dancing-roundly.trycloudflare.app
// Request will be routed directly to the webserver running on the sandbox.const req = await fetch(`${tunnel.url}/api/users`); // => GET http://localhost:8000/api/users
```

Cloudflare Tunnel support currently has the following limitations:

* No control over generated URL.
* No authentication mechanism beyond randomly generated URL.
* Each URL uses an additional `cloudflared` process on the sandbox.

Production requires custom domain

We are working on production deployments, custom hostnames and authentication for Cloudflare Tunnel support. In the mean time we recommend using `exposePort()` and `proxyToSandbox()` documented below under [Production usage, stable URLs and custom domains](#).

See the [tunnels API reference](https://developers.cloudflare.com/sandbox/api/tunnels/) for the full API and feature set.

# Production usage, stable URLs & custom domains

For production use we recommend using the `exposePort()` API and routing traffic through your worker.

Production requires custom domain

Preview URLs work in local development without configuration. For production, you need a custom domain with wildcard DNS routing. See [Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/).

Preview URLs provide public HTTPS access to services running inside sandboxes. When you expose a port, you get a unique URL that proxies requests to your service.

TypeScript

```
// Extract hostname from requestconst { hostname } = new URL(request.url);
await sandbox.startProcess("python -m http.server 8000");const exposed = await sandbox.exposePort(8000, { hostname });
console.log(exposed.url);// Production: https://8000-sandbox-id-abc123random4567.yourdomain.com// Local dev: http://8000-sandbox-id-abc123random4567.localhost:{port}/
```

## URL Format

**Production**: `https://{port}-{sandbox-id}-{token}.yourdomain.com`

* With auto-generated token: `https://8080-abc123-random16chars12.yourdomain.com`
* With custom token: `https://8080-abc123-my_api_v1.yourdomain.com`

**Local development**: `http://{port}-{sandbox-id}-{token}.localhost:{dev-server-port}`

## Token Types

### Auto-generated tokens (default)

When no custom token is specified, a random 16-character token is generated:

TypeScript

```
const exposed = await sandbox.exposePort(8000, { hostname });// https://8000-sandbox-id-abc123random4567.yourdomain.com
```

URLs with auto-generated tokens change when you unexpose and re-expose a port.

### Custom tokens for stable URLs

For production deployments or shared URLs, specify a custom token to maintain consistency across container restarts:

TypeScript

```
const stable = await sandbox.exposePort(8000, {  hostname,  token: "api_v1",});// https://8000-sandbox-id-api_v1.yourdomain.com// Same URL every time ✓
```

**Token requirements:**

* 1-16 characters long
* Lowercase letters (a-z), numbers (0-9), and underscores (\_) only
* Must be unique within each sandbox

**Use cases for custom tokens:**

* Production APIs with stable endpoints
* Sharing demo URLs with external users
* Documentation with consistent examples
* Integration testing with predictable URLs

## ID Case Sensitivity

Preview URLs extract the sandbox ID from the hostname to route requests. Since hostnames are case-insensitive (per RFC 3986), they're always lowercased: `8080-MyProject-123.yourdomain.com` becomes `8080-myproject-123.yourdomain.com`.

**The problem**: If you create a sandbox with `"MyProject-123"`, it exists as a Durable Object with that exact ID. But the preview URL routes to `"myproject-123"` (lowercased from the hostname). These are different Durable Objects, so your sandbox is unreachable via preview URL.

TypeScript

```
// Problem scenarioconst sandbox = getSandbox(env.Sandbox, "MyProject-123");// Durable Object ID: "MyProject-123"await sandbox.exposePort(8080, { hostname });// Preview URL: 8080-myproject-123-token123.yourdomain.com// Routes to: "myproject-123" (different DO - doesn't exist!)
```

**The solution**: Use `normalizeId: true` to lowercase IDs when creating sandboxes:

TypeScript

```
const sandbox = getSandbox(env.Sandbox, "MyProject-123", {  normalizeId: true,});// Durable Object ID: "myproject-123" (lowercased)// Preview URL: 8080-myproject-123-token123.yourdomain.com// Routes to: "myproject-123" (same DO - works!)
```

Without `normalizeId: true`, `exposePort()` throws an error when the ID contains uppercase letters.

**Best practice**: Use lowercase IDs from the start (`'my-project-123'`). See [Sandbox options - normalizeId](https://developers.cloudflare.com/sandbox/configuration/sandbox-options/#normalizeid) for details.

## Request Routing

You must call `proxyToSandbox()` first in your Worker's fetch handler to route preview URL requests:

TypeScript

```
import { proxyToSandbox, getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {  async fetch(request, env) {    // Handle preview URL routing first    const proxyResponse = await proxyToSandbox(request, env);    if (proxyResponse) return proxyResponse;
    // Your application routes    // ...  },};
```

Requests flow: Browser → Your Worker → Durable Object (sandbox) → Your Service.

## Multiple Ports

Expose multiple services simultaneously:

TypeScript

```
// Extract hostname from requestconst { hostname } = new URL(request.url);
await sandbox.startProcess("node api.js"); // Port 3000await sandbox.startProcess("node admin.js"); // Port 3001
const api = await sandbox.exposePort(3000, { hostname, name: "api" });const admin = await sandbox.exposePort(3001, { hostname, name: "admin" });
// Each gets its own URL with unique tokens:// https://3000-abc123-random16chars01.yourdomain.com// https://3001-abc123-random16chars02.yourdomain.com
```

## What Works

* HTTP/HTTPS requests
* WebSocket connections
* Server-Sent Events
* All HTTP methods (GET, POST, PUT, DELETE, etc.)
* Request and response headers

## What Does Not Work

* Raw TCP/UDP connections
* Custom protocols (must wrap in HTTP)
* Ports outside range 1024-65535
* Port 3000 (used internally by the SDK)

## WebSocket Support

Preview URLs support WebSocket connections. When a WebSocket upgrade request hits an exposed port, the routing layer automatically handles the connection handshake.

TypeScript

```
// Extract hostname from requestconst { hostname } = new URL(request.url);
// Start a WebSocket serverawait sandbox.startProcess("bun run ws-server.ts 8080");const { url } = await sandbox.exposePort(8080, { hostname });
// Clients connect using WebSocket protocol// Browser: new WebSocket('wss://8080-abc123-token123.yourdomain.com')
// Your Worker routes automaticallyexport default {  async fetch(request, env) {    const proxyResponse = await proxyToSandbox(request, env);    if (proxyResponse) return proxyResponse;  },};
```

For custom routing scenarios where your Worker needs to control which sandbox or port to connect to based on request properties, see `wsConnect()` in the [Ports API](https://developers.cloudflare.com/sandbox/api/ports/#wsconnect).

## Security

Warning

Preview URLs are publicly accessible by default, but require a valid access token that is generated when you expose a port.

**Built-in security**:

* **Token-based access** \- Each exposed port gets a unique token in the URL (for example, `https://8080-sandbox-abc123token456.yourdomain.com`)
* **HTTPS in production** \- All traffic is encrypted with TLS. Certificates are provisioned automatically for first-level wildcards (`*.yourdomain.com`). If your worker runs on a subdomain, see the [TLS note in Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/).
* **Unpredictable URLs** \- Auto-generated tokens are randomly generated and difficult to guess
* **Token collision prevention** \- Custom tokens are validated to ensure uniqueness within each sandbox

**Add application-level authentication**:

For additional security, implement authentication within your application:

Python

```
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/data')def get_data():    # Check for your own authentication token    auth_token = request.headers.get('Authorization')    if auth_token != 'Bearer your-secret-token':        abort(401)    return {'data': 'protected'}
```

This adds a second layer of security on top of the URL token.

## Troubleshooting

### URL Not Accessible

Check if service is running and listening:

TypeScript

```
// 1. Is service running?const processes = await sandbox.listProcesses();
// 2. Is port exposed?const ports = await sandbox.getExposedPorts();
// 3. Is service binding to 0.0.0.0 (not 127.0.0.1)?// Good:app.run((host = "0.0.0.0"), (port = 3000));
// Bad (localhost only):app.run((host = "127.0.0.1"), (port = 3000));
```

### Production Errors

For custom domain issues, see [Production Deployment troubleshooting](https://developers.cloudflare.com/sandbox/guides/production-deployment/#troubleshooting).

### Local Development

Local development limitation

When using `wrangler dev`, you must expose ports in your Dockerfile:

```
FROM docker.io/cloudflare/sandbox:0.3.3
# Required for local developmentEXPOSE 3000EXPOSE 8080
```

Without `EXPOSE`, you'll see: `connect(): Connection refused: container port not found`

This is **only required for local development**. In production, all container ports are automatically accessible.

## Related Resources

* [Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/) \- Set up custom domains for production
* [Expose Services](https://developers.cloudflare.com/sandbox/guides/expose-services/) \- Practical patterns for exposing ports
* [Ports API](https://developers.cloudflare.com/sandbox/api/ports/) \- Complete API reference
* [Tunnels API](https://developers.cloudflare.com/sandbox/api/tunnels/) \- Zero-config `*.trycloudflare.com` URLs as an alternative for development
* [Security Model](https://developers.cloudflare.com/sandbox/concepts/security/) \- Security best practices

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/concepts/preview-urls/#page","headline":"Preview URLs · Cloudflare Sandbox SDK docs","description":"Sandbox SDK preview URLs provide public HTTPS access to services running inside sandboxes.","url":"https://developers.cloudflare.com/sandbox/concepts/preview-urls/","inLanguage":"en","image":"https://developers.cloudflare.com/dev-products-preview.png","dateModified":"2026-05-26","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/sandbox/","name":"Sandbox SDK"}},{"@type":"ListItem","position":3,"item":{"@id":"/sandbox/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/sandbox/concepts/preview-urls/","name":"Preview URLs"}}]}
```
