A zero-config local tunnel that's simple, lightweight, and secure.
- Simple, lightweight local tunnel
- Security built-in, including HTTPS
- Smart subdomains (APP_NAME-aware, memorable random names, auto-collision handling)
- Auto DNS resolution (bypasses broken system DNS on macOS
.devTLD) - IAC, self-hostable (via AWS)
- CLI & Library
bun install -d localtunnelsThere are two ways of using this local tunnel: as a library or as a CLI.
Given the npm package is installed:
import { startLocalTunnel } from 'localtunnels'
const client = await startLocalTunnel({
port: 3000,
// subdomain: 'myapp', // optional, see Subdomains below
// verbose: true, // optional
})
console.log(`Tunnel URL: ${client.getTunnelUrl()}`)
// later...
client.disconnect()Or use the TunnelClient class directly:
import { TunnelClient } from 'localtunnels'
const client = new TunnelClient({
host: 'localtunnel.dev',
port: 443,
secure: true,
localPort: 3000,
})
client.on('connected', (info) => {
console.log(`Public URL: ${info.url}`)
})
await client.connect()# Expose local port 3000 (default)
localtunnels start
# Expose a specific port
localtunnels start --port 8080
# Request a specific subdomain
localtunnels start --port 3000 --subdomain myapp
# Use a custom tunnel server
localtunnels start --port 3000 --server mytunnel.example.com
# Disable auto DNS resolution
localtunnels start --port 3000 --no-manage-hosts
# Show all requests
localtunnels start --port 3000 --verboseOutput:
Connecting to localtunnel.dev...
Public: https://swift-fox.localtunnel.dev
Forwarding: https://swift-fox.localtunnel.dev -> http://localhost:3000
Press Ctrl+C to stop sharing
localtunnels uses a smart subdomain resolution chain:
- Explicit flag:
--subdomain myapporsubdomain: 'myapp'in code APP_NAMEenv var: automatically slugified (e.g.My Cool Appbecomesmy-cool-app)- Random memorable name: adjective-noun combos like
swift-fox,bold-comet,lazy-elk
If a subdomain is already in use by another client, localtunnels automatically appends an incrementing suffix:
myappis taken -> triesmyapp-2myapp-2is taken -> triesmyapp-3- and so on...
This happens transparently — no crashes, no manual intervention needed.
# Uses APP_NAME env var if set
APP_NAME="My App" localtunnels start --port 3000
# -> https://my-app.localtunnel.dev
# Explicit subdomain
localtunnels start --port 3000 --subdomain demo
# -> https://demo.localtunnel.dev
# Random memorable name (no APP_NAME, no --subdomain)
localtunnels start --port 3000
# -> https://bold-comet.localtunnel.devOn some machines (especially macOS with .dev TLD), the system DNS resolver can't reach localtunnel.dev even though tools like dig and nslookup work fine. localtunnels detects this automatically and resolves the server IP via DNS-over-HTTPS (Cloudflare) or dig @8.8.8.8, then connects directly to the IP.
This is on by default. Disable with --no-manage-hosts or manageHosts: false.
Start your own tunnel server:
localtunnels server --port 8080 --domain mytunnel.example.comOr deploy to AWS:
localtunnels deploy --domain mytunnel.example.com --key-name my-keypairlocaltunnels ships with a benchmark suite built on mitata. The suite covers utility functions, connection lifecycle, request throughput, latency distribution, scalability under load, and cross-tool comparisons.
# Run all benchmarks
bun benchmarks/index.ts
# Run individual suites
bun benchmarks/utils.ts # Utility function microbenchmarks
bun benchmarks/connection.ts # Connection lifecycle
bun benchmarks/throughput.ts # Request forwarding throughput
bun benchmarks/latency.ts # End-to-end latency distribution
bun benchmarks/scalability.ts # Multi-connection scalability
bun benchmarks/comparison.ts # Cross-tool comparisonMeasured on Apple M3 Pro, bun 1.3.10 (arm64-darwin). Other tools tested: cloudflared 2026.2.0, ngrok 3.36.1, bore-cli 0.6.0, frpc 0.67.0.
Real end-to-end request forwarding through each tool's tunnel. localtunnels runs on localhost, bore routes through bore.pub.
GET / (plain text):
| Tool | avg | vs direct |
|---|---|---|
| Direct (no tunnel) | 35.67 µs | 1x (baseline) |
| localtunnels | 105.97 µs | 2.97x |
| bore | 188.60 ms | 5,290x |
GET /json (10-item JSON array):
| Tool | avg | vs direct |
|---|---|---|
| Direct (no tunnel) | 30.67 µs | 1x (baseline) |
| localtunnels | 109.58 µs | 3.57x |
| bore | 180.11 ms | 5,872x |
POST 1 KB body:
| Tool | avg | vs direct |
|---|---|---|
| Direct (no tunnel) | 29.43 µs | 1x (baseline) |
| localtunnels | 106.89 µs | 3.63x |
| bore | 180.74 ms | 6,143x |
10 Concurrent Requests (GET /json):
| Tool | avg | vs direct |
|---|---|---|
| Direct (no tunnel) | 108.05 µs | 1x (baseline) |
| localtunnels | 592.58 µs | 5.48x |
| bore | 188.46 ms | 1,744x |
| Tool | Time to tunnel ready |
|---|---|
| localtunnels | ~324 µs |
| bore | 195 ms |
| Cloudflare Tunnels | 3,969 ms |
| Tool | Strategy | Example Output | avg | vs localtunnels |
|---|---|---|---|---|
| localtunnels | Adjective-noun | fast-deer, quick-surf, fond-opal |
3.00 ns | 1x |
| frp | Counter prefix | tunnel-1, tunnel-2, tunnel-3 |
25.66 ns | 8.55x slower |
| Cloudflare Tunnels | UUID prefix | a7ed76b1, ee76358d, d25abca3 |
42.69 ns | 14.23x slower |
| Expose | UUID slug | a432cef06efa, 15b07c93bc27 |
96.60 ns | 32.20x slower |
| bore | Short hex | df28e3, 1cb723, 06189e |
191.94 ns | 63.98x slower |
| ngrok | Random hex | c2a8b92e, 5c219911, 65a2aba4 |
279.09 ns | 93.03x slower |
| Tool | Strategy | avg | vs fastest |
|---|---|---|---|
| frp | Counter-based | 22.19 ns | 1x |
| ngrok / Cloudflare Tunnels | crypto.randomUUID() |
30.94 ns | 1.39x |
| localtunnels | crypto.randomUUID().substring() |
42.42 ns | 1.91x |
| bore | crypto.getRandomValues |
358.60 ns | 16.16x |
localtunnels uses WebSocket + JSON. bore and frp use binary protocols. This measures per-message encode/decode cost.
| Tool | Operation | avg | vs fastest |
|---|---|---|---|
| localtunnels | JSON serialize | 123.05 ns | 1x |
| bore / frp | Binary header encode | 159.04 ns | 1.29x |
| bore / frp | Binary header decode | 176.33 ns | 1.43x |
| localtunnels | JSON parse | 434.87 ns | 3.53x |
| Tool | Strategy | avg | vs fastest |
|---|---|---|---|
| frp / bore(Go-style) | Enum-based | 2.20 ns | 1x |
| localtunnels | String-based | 2.35 ns | 1.07x |
| ngrok / Expose | Object-based | 3.54 ns | 1.61x |
| Payload | Direct | localtunnels | Overhead |
|---|---|---|---|
| 20 B | 33 µs | 102 µs | 3.1x |
| 1 KB | 30 µs | 116 µs | 3.8x |
| 64 KB | 47 µs | 350 µs | 7.5x |
| 512 KB | 144 µs | 2.08 ms | 14.4x |
| 1 MB | 234 µs | 4.16 ms | 17.8x |
| Scenario | avg |
|---|---|
| Instant response (pure overhead) | 182 µs |
| JSON API (10-item array) | 210 µs |
| With 10 ms backend | 10.44 ms (1.05x over direct) |
| With 50 ms backend | 50.51 ms (1.01x over direct) |
| Active Tunnels | Request Latency (avg) |
|---|---|
| 1 | 235 µs |
| 10 | 237 µs |
| 50 | 234 µs |
| Operation | avg |
|---|---|
| Server start + stop | 325 µs |
| Client connect + register + disconnect | 296 µs |
| 5 clients sequential | 1.61 ms |
| 5 clients concurrent | 921 µs |
The cross-tool comparison auto-detects installed tunneling tools (cloudflared, ngrok, bore, frpc, expose) and includes them in results. See the benchmark documentation for full results, suite descriptions, and methodology.
bun testPlease see our releases page for more information on what has changed recently.
Please review the Contributing Guide for details.
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
For casual chit-chat with others using this package:
Join the Stacks Discord Server
“Software that is free, but hopes for a postcard.” We love receiving postcards from around the world showing where localtunnels is being used! We showcase them on our website too.
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎
We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
The MIT License (MIT). Please see LICENSE for more information.
Made with 💙
