Vite HMR Through a Tunnel (Config Checklist)
Deep dive: For symptoms, WebSocket theory, and troubleshooting, read Vite HMR Over an HTTPS Tunnel. This guide is the short configuration checklist.
Why Vite HMR breaks through tunnels
Vite's dev server uses WebSockets for Hot Module Replacement (HMR). When you access your app through a tunnel URL, two things differ from normal localhost development:
- Host header — the browser sends
k7m2x9p4q1.localtolink.com, notlocalhost - WebSocket connection — HMR must connect via
wss://on port 443 (the tunnel terminates TLS)
Without configuration, Vite may block the foreign hostname or attempt HMR on the wrong port/protocol. The fix is a few lines in vite.config.js.
Prerequisites
- Vite project (React, Vue, Svelte, etc.)
- Node.js 22.14+ for LocalToLink
- Dev server on port 5173 (Vite default) or your configured port
Step 1: Start Vite and the tunnel
Terminal 1:
npm run dev
Terminal 2:
npx localtolink --port 5173
Note the public URL, e.g. https://k7m2x9p4q1.localtolink.com.
Step 2: Configure Vite
Add to vite.config.js or vite.config.ts:
import { defineConfig } from "vite";
export default defineConfig({
server: {
allowedHosts: [".localtolink.com"],
hmr: {
clientPort: 443,
protocol: "wss",
},
},
});
Restart Vite after saving — config is read at startup.
What each setting does
| Setting | Purpose |
|---|---|
allowedHosts: [".localtolink.com"] | Allows the dev server to respond to tunnel hostnames (Vite 5.4+ host check) |
hmr.clientPort: 443 | HMR WebSocket connects via HTTPS port — tunnel terminates TLS at edge |
hmr.protocol: "wss" | Secure WebSocket through the tunnel |
The leading dot in .localtolink.com matches all subdomains, so you do not update config when LocalToLink assigns a new random subdomain.
Step 3: Verify HMR works
- Open the tunnel URL in a browser
- Continue through any safety prompt if shown (first visit only)
- Edit a component file — e.g. change button text
- Confirm the UI updates without a full page refresh
- Check browser DevTools → Network → WS tab for an active HMR WebSocket
If you see a full page reload instead of hot update, HMR is falling back — check the troubleshooting section.
Phone testing with QR code
LocalToLink prints a QR code in the terminal. Scan it to test HMR on a physical device:
npx localtolink --port 5173
# QR code prints automatically; use --no-qr to hide
Mobile HMR confirms touch interactions and responsive layout with real hot reload. See Test Localhost on Your Phone.
API proxy considerations
If your Vite config proxies API requests to a backend:
export default defineConfig({
server: {
allowedHosts: [".localtolink.com"],
hmr: { clientPort: 443, protocol: "wss" },
proxy: {
"/api": "http://localhost:8080",
},
},
});
The proxy runs server-side on your machine — it works through the tunnel because Vite handles the proxy locally before responding to the browser.
WebSocket support
LocalToLink forwards WebSocket upgrades bidirectionally. Your local Vite server handles the HMR protocol — the tunnel transports frames transparently. No extra configuration is needed on the tunnel side.
This also applies to custom WebSocket endpoints in your app, not just HMR. See WebSocket Through a Tunnel.
Browser interstitial
When the visitor interstitial policy is enabled, the first browser visit to a new subdomain may show LocalToLink's Opening a shared link page — tap Continue once (24-hour cookie per subdomain). WebSocket and HMR traffic from dev tooling often behaves like non-browser or already-bypassed traffic; if HMR stalls, open the tunnel URL in a tab and continue through the prompt, or send localtolink-skip-browser-warning in automated tests.
Session limits
| Limit | Impact on HMR |
|---|---|
| 2 hours | Restart tunnel and refresh browser when session expires |
| 1 GB bandwidth | Sufficient for development HMR |
| 10 req/s | HMR updates are low volume — no issue |
| 20 concurrent connections | Fine for solo or small team testing |
Troubleshooting
| Issue | Fix |
|---|---|
| HMR WebSocket connection failed | Verify clientPort: 443 and protocol: "wss" |
Blocked host error | Add .localtolink.com to allowedHosts |
| Full page reload only | Check browser console for WS errors; restart Vite |
| HMR works on localhost, not tunnel | Open app via tunnel URL, not localhost |
| WS connects then disconnects | Tunnel session may have expired — restart both |
| Mixed content warnings | Ensure you use https:// tunnel URL, not http:// |
Alternative: preview mode
For demos where HMR is not needed, build and preview:
npm run build
npm run preview
npx localtolink --port 4173
Vite preview serves static production assets — no HMR, but no WebSocket config needed.
Framework-specific notes
React + Vite: HMR config above applies directly.
Vue + Vite: Same config. Vue SFC hot reload uses the same HMR WebSocket.
Svelte + Vite: Same config via @sveltejs/vite-plugin-svelte.
Next.js users: Next.js is not Vite-based — see Next.js Dev Server Through a Tunnel.
FAQ
Does this work with Vite 6?
Yes. allowedHosts and hmr settings are stable across recent Vite versions.
Can I use a custom HMR path?
If you changed Vite's default HMR path, ensure your tunnel forwards the path correctly. Default config works without changes.
Why port 443 for HMR?
The browser connects to the tunnel's HTTPS endpoint on port 443. The tunnel forwards WebSocket frames to your local Vite server on 5173.
See Expose Localhost to the Internet for tunnel basics.
Try it now
Start a tunnel from your project directory — no account required.
$ npx --yes localtolink@latest