WebSocket Through a LocalToLink Tunnel
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:
- Vite and Next.js Hot Module Replacement (HMR)
- Chat applications and live dashboards
- Twilio Media Streams
- Socket.io and ws-based APIs
- Collaborative editing tools
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
- Client sends HTTP GET with
Upgrade: websocketto the tunnel URL - LocalToLink forwards the upgrade request to your local server
- Your server completes the WebSocket handshake
- 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 case | Configuration |
|---|---|
| Vite HMR | hmr.protocol: "wss", hmr.clientPort: 443 — see Vite guide |
| Next.js Fast Refresh | allowedDevOrigins: ["*.localtolink.com"] — see Next.js guide |
| Chat / real-time apps | Connect clients to wss://SUBDOMAIN.localtolink.com/your-ws-path |
| Twilio Media Streams | Set stream URL to wss://SUBDOMAIN.localtolink.com/twilio/stream |
| Socket.io | Configure 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
- Raw TCP tunnels — only HTTP and WebSocket upgrades (not arbitrary TCP protocols)
- WebSocket inspector — use browser DevTools Network → WS tab or server logs
- UDP or QUIC — HTTP-based tunnel only
Troubleshooting
| Issue | Fix |
|---|---|
| WebSocket connection failed | Confirm local server handles upgrades on the forwarded port |
| Works on localhost, not tunnel | Client must connect to wss:// tunnel URL, not ws://localhost |
| HMR WebSocket fails | See framework-specific guides (Vite/Next.js config) |
| Connection drops unexpectedly | Session may have ended — restart tunnel |
| Upgrade blocked or denied | Session 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:
- Confirm HTTP works first —
curl -I https://k7m2x9p4q1.localtolink.com/should return your app headers. - Use
wss://in the client — browsers block mixed content if the page is HTTPS but the socket targetsws://localhost. - Check the upgrade path — some frameworks mount sockets on
/socket.ioor a custom path; tunnel forwarding is path-preserving. - Inspect in DevTools — Network → WS shows close codes; compare with a direct localhost connection.
- 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