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.
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/...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.
You must call proxyToSandbox() first in your Worker's fetch handler to route preview URL requests:
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.
Expose multiple services simultaneously:
await sandbox.startProcess("node api.js"); // Port 3000await 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- HTTP/HTTPS requests
- WebSocket connections
- Server-Sent Events
- All HTTP methods (GET, POST, PUT, DELETE, etc.)
- Request and response headers
- Raw TCP/UDP connections
- Custom protocols (must wrap in HTTP)
- Ports outside range 1024-65535
- Port 3000 (used internally by the SDK)
Preview URLs support WebSocket connections. When a WebSocket upgrade request hits an exposed port, the routing layer automatically handles the connection handshake.
// Start a WebSocket serverawait sandbox.startProcess("bun run ws-server.ts 8080");const { exposedAt } = await sandbox.exposePort(8080);
// Clients connect using WebSocket protocol// Browser: new WebSocket('wss://8080-abc123.example.com')
// Your Worker routes automaticallyexport default { async fetch(request, env) { return proxyToSandbox(request, env.Sandbox, "sandbox-id"); },};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.
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:
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.
Check if service is running and listening:
// 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));For custom domain issues, see Production Deployment troubleshooting.
- Production Deployment - Set up custom domains for production
- Expose Services - Practical patterns for exposing ports
- Ports API - Complete API reference
- Security Model - Security best practices
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark