# localtunnels

> A simple and smart tunneling solution. Expose your localhost to the world.

Latest version **0.2.13** (published 2026-08-19) · MIT license · 0 weekly downloads

## Install

```sh
npm install localtunnels
pnpm add localtunnels
yarn add localtunnels
bun add localtunnels
```

Provides the commands `lt`, `localtunnel`.

## Health

**Score 75/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; has provenance; recently updated; high maintenance score; high quality score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.2.13 |
| Published | 2026-08-19 |
| First published | 2024-12-10 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM |
| Dependencies | 1 |
| Unpacked size | 486.4 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 22 |
| Author | Chris Breuer <chris@stacksjs.com> |
| Maintainers | chrisbreuer |
| Keywords | local, tunnel, localtunnel, ngrok, self-hosted, websocket, proxy, expose, localhost, vpn, wireguard, bun, stacks, typescript, javascript |

## Links

- npm: https://www.npmjs.com/package/localtunnels
- Repository: https://github.com/stacksjs/localtunnels
- Issues: https://github.com/stacksjs/localtunnels/issues
- npm.io page: https://npm.io/package/localtunnels

## Dependencies (1)

- [bunfig](https://npm.io/package/bunfig.md) ^0.15.6

## Alternatives

- [@opentelemetry/exporter-zipkin](https://npm.io/package/@opentelemetry/exporter-zipkin.md) — 14.8M weekly downloads
- [pusher-js](https://npm.io/package/pusher-js.md) — 2.0M weekly downloads
- [browserify](https://npm.io/package/browserify.md) — 1.7M weekly downloads
- [sqs-consumer](https://npm.io/package/sqs-consumer.md) — 1.7M weekly downloads
- [@sanity/eventsource](https://npm.io/package/@sanity/eventsource.md) — 930.8K weekly downloads

## Recent versions

- 0.2.13 (latest) — 2026-08-19
- 0.2.12 — 2026-07-28
- 0.2.11 — 2026-07-26
- 0.2.10 — 2026-07-26
- 0.2.7 — 2026-02-27
- 0.2.6 — 2026-02-26
- 0.2.5 — 2026-02-26
- 0.2.3 — 2026-02-26
- 0.1.1 — 2024-12-10

## README

<p align="center"><img src="https://github.com/stacksjs/localtunnels/blob/main/.github/art/cover.jpg?raw=true" alt="Social Card of this repo"></p>

[![npm version][npm-version-src]][npm-version-href]
[![GitHub Actions][github-actions-src]][github-actions-href]
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
<!-- [![npm downloads][npm-downloads-src]][npm-downloads-href] -->
<!-- [![Codecov][codecov-src]][codecov-href] -->

# localtunnels

> A zero-config local tunnel that's simple, lightweight, and secure.

## Features

- Simple, lightweight local tunnel
- Security built-in, including HTTPS
- Binary-safe forwarding _(fonts, archives, media & uploads survive byte-for-byte)_
- Standard proxy headers _(`X-Forwarded-For` / `-Host` / `-Proto` for your local app)_
- Smart subdomains _(APP_NAME-aware, memorable random names, auto-collision handling)_
- Auto DNS resolution _(bypasses broken system DNS on macOS `.dev` TLD)_
- Built-in devtools & Prometheus metrics _(per-tunnel request log, `/metrics`, `/status`)_
- WireGuard-style VPN mode _(private layer-3 mesh, powered by a Zig crypto core)_
- Self-hostable anywhere, with IaC deploys via [ts-cloud](https://github.com/stacksjs/ts-cloud) _(AWS & Hetzner today, more providers as ts-cloud grows)_
- CLI & Library

## Install

```sh
bun install -d localtunnels
```

## Get Started

There are two ways of using this local tunnel: _as a library or as a CLI._

### Library

Given the npm package is installed:

```ts
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:

```ts
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()
```

### CLI

```sh
# 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 --verbose

# TUNNEL_SERVER and TUNNEL_SUBDOMAIN env vars set defaults for the flags
TUNNEL_SERVER=mytunnel.example.com TUNNEL_SUBDOMAIN=myapp localtunnels start --port 3000
```

Output:

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

## Subdomains

localtunnels uses a smart subdomain resolution chain:

1. **Explicit flag**: `--subdomain myapp` or `subdomain: 'myapp'` in code
2. **`APP_NAME` env var**: automatically slugified _(e.g. `My Cool App` becomes `my-cool-app`)_
3. **Random memorable name**: adjective-noun combos like `swift-fox`, `bold-comet`, `lazy-elk`

### Collision Handling

If a subdomain is already in use by another client, localtunnels automatically appends an incrementing suffix:

- `myapp` is taken -> tries `myapp-2`
- `myapp-2` is taken -> tries `myapp-3`

After a few suffix attempts it switches to fresh random names, and it fails with a clear error (rather than retrying forever) if the server keeps rejecting.

This happens transparently — no crashes, no manual intervention needed.

### Examples

```sh
# 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.dev
```

## DNS Resolution

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

## Self-Hosting

The tunnel server runs anywhere Bun runs — a VPS, a homelab box, a container:

```sh
localtunnels server --port 8080 --domain mytunnel.example.com
```

Clients then connect with `--server mytunnel.example.com` (or set `TUNNEL_SERVER`).

### Infrastructure as Code

Cloud deployments are powered by [ts-cloud](https://github.com/stacksjs/ts-cloud), and every deploy takes a provider flag — capabilities aren't tied to a vendor. AWS EC2 and Hetzner Cloud work today; more providers land as ts-cloud grows.

- **Tunnel server** — one command, with optional wildcard Let's Encrypt TLS (Porkbun DNS-01) served by Bun's native TLS:

  ```sh
  # AWS EC2 (also automates Route53 DNS)
  localtunnels deploy:tunnel --domain mytunnel.example.com --enable-ssl

  # Hetzner Cloud (prints the DNS records to create)
  localtunnels deploy:tunnel --provider hetzner --domain mytunnel.example.com --enable-ssl
  ```

- **VPN / exit node** — fully-automated, end-to-end-verified, running the localtunnels WireGuard stack (see [`deploy/`](deploy)):

  ```sh
  bun run deploy:vpn                     # Hetzner (default)
  bun run deploy:vpn -- --provider aws   # AWS EC2
  bun run verify:vpn                     # e2e: handshake, tunnel ping, exit routing
  ```

Tear down anytime with `localtunnels destroy [--provider hetzner]` or `bun run destroy:vpn`.

## VPN Mode

Beyond exposing a single port, localtunnels can join machines into a private,
encrypted layer-3 network — a self-hosted, WireGuard-style VPN. It implements
the actual **WireGuard v1 protocol** (`Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s`)
in a dependency-free **Zig** core (`libltvpn`, consumed from Bun over `bun:ffi`),
with the control plane in TypeScript.

The core is validated for wire-correctness against an independent reference
implementation (`packages/vpn-core/testvectors/wg_ref.py`, itself checked against the
RFC 7748 / 8439 / 7693 vectors): the Zig code produces byte-identical handshake
messages, transport keys, and the canonical WireGuard `InitialChainKey`.

```sh
# Generate this machine's identity (X25519 keypair, stored 0600)
lt vpn keygen

# Verify the native core (handshake + encryption + replay protection)
lt vpn selftest

# See it move encrypted traffic between two peers over real UDP
lt vpn demo

# Run the peer-discovery coordinator, then a self-configuring mesh demo
lt vpn coordinator
lt vpn mesh-demo

# Bring up a real layer-3 interface bridged to a peer (needs root)
sudo lt vpn up --peer <pubkey> --endpoint <host>:51820
```

Features: X25519 identities, ChaCha20-Poly1305 transport with RFC-6479 replay
protection, session rekeying, cryptokey routing (allowed-ips), a coordinator for
zero-config peer discovery and IP assignment, NAT hole punching with an
encrypted relay fallback (the coordinator only ever sees ciphertext), and TUN
devices on macOS (`utun`) and Linux (`/dev/net/tun`).

Build the native library from source (requires [Zig](https://ziglang.org)):

```sh
bun run build:native   # → packages/vpn-core/zig-out/lib/libltvpn.*
bun run test:native    # Zig unit + known-answer + fuzz tests
```

VPN features degrade gracefully: if the native library isn't present, the HTTP
tunnel above is unaffected.

## Benchmarks

localtunnels ships with a benchmark suite built on [mitata](https://github.com/evanwashere/mitata). The suite covers utility functions, connection lifecycle, request throughput, latency distribution, scalability under load, and cross-tool comparisons.

```sh
# 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 comparison
```

### Results

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

#### localtunnels vs Alternatives — Request Forwarding

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 |

#### localtunnels vs Alternatives — Startup Time

| Tool | Time to tunnel ready |
|---|---|
| **localtunnels**|**~324 µs** |
| **bore** | 195 ms |
| **Cloudflare Tunnels** | 3,969 ms |

#### localtunnels vs Alternatives — Subdomain Generation

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

#### localtunnels vs Alternatives — ID Generation

| 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 vs Alternatives — Protocol Overhead

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 |

#### localtunnels vs Alternatives — State Machine

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

#### Throughput (GET, direct vs tunnel)

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

#### Latency

| 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)_ |

#### Scalability

| Active Tunnels | Request Latency (avg) |
|---|---|
| 1 | 235 µs |
| 10 | 237 µs |
| 50 | 234 µs |

#### Connection Lifecycle

| 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](https://localtunnels.sh/benchmarks) for full results, suite descriptions, and methodology.

## Testing

```sh
bun test
```

## Changelog

Please see our [releases](https://github.com/stacksjs/localtunnels/releases) page for more information on what has changed recently.

## Contributing

Please review the [Contributing Guide](https://github.com/stacksjs/contributing) for details.

## Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable:

[Discussions on GitHub](https://github.com/stacksjs/stacks/discussions)

For casual chit-chat with others using this package:

[Join the Stacks Discord Server](https://stacksjs.com/discord)

## Postcardware

“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 🌎

## Sponsors

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.

- [JetBrains](https://www.jetbrains.com/)
- [The Solana Foundation](https://solana.com/)

## Credits

- [Chris Breuer](https://github.com/chrisbbreuer)
- [All Contributors](../../contributors)

## License

The MIT License (MIT). Please see [LICENSE](https://github.com/stacksjs/stacks/tree/main/LICENSE.md) for more information.

Made with 💙

<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/localtunnels?style=flat-square
[npm-version-href]: https://npmjs.com/package/localtunnels
[github-actions-src]: https://img.shields.io/github/actions/workflow/status/stacksjs/localtunnels/ci.yml?style=flat-square&branch=main
[github-actions-href]: https://github.com/stacksjs/localtunnels/actions?query=workflow%3Aci

<!-- [codecov-src]: https://img.shields.io/codecov/c/gh/stacksjs/localtunnels/main?style=flat-square
[codecov-href]: https://codecov.io/gh/stacksjs/localtunnels -->

---
_Source: https://npm.io/package/localtunnels · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
