Skip to content
Cloudflare Docs

Preview URLs

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
await sandbox.startProcess("python -m http.server 8000");
const exposed = await sandbox.exposePort(8000);
console.log(exposed.exposedAt);
// Production: https://8000-abc123.example.com
// Local dev: http://localhost:8787/...

URL Format

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

  • Port 8080: https://8080-abc123.example.com
  • Port 3000: https://3000-abc123.example.com

Local development: http://localhost:8787/...

Preview URLs remain stable while a port is exposed and can be shared during that time. However, if you unexpose and re-expose a port, a new random token is generated and the URL changes. For persistent URLs, keep ports exposed for the duration you need them accessible.

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 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
await sandbox.startProcess("node api.js"); // Port 3000
await sandbox.startProcess("node admin.js"); // Port 3001
const api = await sandbox.exposePort(3000, { name: "api" });
const admin = await sandbox.exposePort(3001, { name: "admin" });
// Each gets its own URL:
// https://3000-abc123.example.com
// https://3001-abc123.example.com

What Works

  • HTTP/HTTPS requests
  • 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)
  • WebSocket connections
  • Ports outside range 1024-65535
  • Port 3000 (used internally by the SDK)

Security

Built-in security:

  • Token-based access - Each exposed port gets a unique token in the URL (for example, https://8080-sandbox-abc123token.example.com)
  • HTTPS in production - All traffic is encrypted with automatic TLS
  • Unpredictable URLs - Tokens are randomly generated and difficult to guess

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.

Local Development