FeaturesGuidesBlogDocumentationFreeGet Started

WebSocket Through a LocalToLink Tunnel

LocalToLink supports bidirectional WebSocket passthrough for HMR, chat apps, and real-time APIs.

WebSocket support on the free tier

LocalToLink forwards WebSocket upgrade requests bidirectionally. Your local app handles the WebSocket protocol — the tunnel transports frames transparently without parsing or modifying message content.

This enables real-time features through a public URL:

npx localtolink --port 3000

Clients connect to:

wss://k7m2x9p4q1.localtolink.com/ws

The CLI also prints websocketUrl in --json output:

npx localtolink --json --port 3000 | jq .websocketUrl

How it works

  1. Client sends HTTP GET with Upgrade: websocket to the tunnel URL
  2. LocalToLink forwards the upgrade request to your local server
  3. Your server completes the WebSocket handshake
  4. Subsequent frames flow bidirectionally through the tunnel

No WebSocket-specific configuration is needed on the tunnel side. Configure your local app and client to use wss:// and the tunnel hostname.

Common use cases

Use caseConfiguration
Vite HMRhmr.protocol: "wss", hmr.clientPort: 443 — see Vite guide
Next.js Fast RefreshallowedDevOrigins: ["*.localtolink.com"] — see Next.js guide
Chat / real-time appsConnect clients to wss://SUBDOMAIN.localtolink.com/your-ws-path
Twilio Media StreamsSet stream URL to wss://SUBDOMAIN.localtolink.com/twilio/stream
Socket.ioConfigure client to connect via tunnel origin

Client connection example

Browser JavaScript:

const ws = new WebSocket("wss://k7m2x9p4q1.localtolink.com/ws");

ws.onopen = () => ws.send("hello");
ws.onmessage = (event) => console.log(event.data);
ws.onerror = (err) => console.error("WebSocket error:", err);

Ensure your local server handles WebSocket upgrades on the same port LocalToLink forwards to.

Server-side example (Node.js ws)

import { WebSocketServer } from "ws";
import { createServer } from "http";

const server = createServer(app);
const wss = new WebSocketServer({ server, path: "/ws" });

wss.on("connection", (ws) => {
  ws.on("message", (data) => ws.send(`echo: ${data}`));
});

server.listen(3000);

Start the tunnel pointing to port 3000. Clients connect via wss://SUBDOMAIN.localtolink.com/ws.

Browser interstitial and WebSockets

When the visitor interstitial policy is enabled, open the tunnel URL once in a normal browser tab and tap Continue on a new subdomain before relying on in-page WebSockets. Server-side and CLI clients using non-browser user agents usually skip the page. For Playwright or Cypress, set the header localtolink-skip-browser-warning or complete the continue URL once.

Limits relevant to WebSockets

WebSockets are supported on the free tier. WebSocket traffic counts toward the same session duration and bandwidth caps as HTTP. See Documentation for current limits.

What is not supported

Troubleshooting

IssueFix
WebSocket connection failedConfirm local server handles upgrades on the forwarded port
Works on localhost, not tunnelClient must connect to wss:// tunnel URL, not ws://localhost
HMR WebSocket failsSee framework-specific guides (Vite/Next.js config)
Connection drops unexpectedlySession may have ended — restart tunnel
Upgrade blocked or deniedSession or usage limit may apply — restart tunnel

Comparison with localtunnel

localtunnel is HTTP-focused. WebSocket support is unreliable (GitHub issue #297). LocalToLink explicitly supports WebSocket passthrough for development workflows like HMR and real-time apps.

Debugging WebSocket upgrades

When a WebSocket fails only through the tunnel:

  1. Confirm HTTP works first — curl -I https://k7m2x9p4q1.localtolink.com/ should return your app headers.
  2. Use wss:// in the client — browsers block mixed content if the page is HTTPS but the socket targets ws://localhost.
  3. Check the upgrade path — some frameworks mount sockets on /socket.io or a custom path; tunnel forwarding is path-preserving.
  4. Inspect in DevTools — Network → WS shows close codes; compare with a direct localhost connection.
  5. Framework config — Vite and Next.js need extra host/HMR settings; see Vite HMR checklist and Next.js allowedDevOrigins.

Tier B note: this guide covers passthrough behavior. For production ingress or TCP-level protocols, use a production connector — not a dev tunnel.

Load balancers and sticky sessions

If your local app assumes sticky sessions on WebSocket reconnect, remember each new tunnel session may get a new subdomain. Point clients at the current wss:// URL from CLI output or --json. For HMR-specific config, pair this guide with Vite HMR Through a Tunnel.

FAQ

Does Socket.io work?
Yes. Configure the Socket.io client to connect via the tunnel origin with wss transport.

Can I load test WebSockets through the tunnel?
Free-tier sessions are for development, not load testing.

Do WebSocket connections count toward session limits?
Yes. WebSocket traffic counts toward the same session duration and bandwidth caps as HTTP.

See Vite HMR Through a Tunnel and Expose Localhost to the Internet.

Try it now

Start a tunnel from your project directory — no account required.

$ npx --yes localtolink@latest

See the full CLI reference for flags and exit codes.

LocalToLink CLI 1.0.1 — verified 2026-09-05