# Fix hot reload through a tunnel

> Hot reload through ngrok, Cloudflare or qURL fails for one of three reasons: a blocked host, a WebSocket that doesn’t get through, or no Server-Sent Events.

Source: https://layerv.ai/guides/fix-hot-reload-through-a-tunnel/

---

[← All guides](https://layerv.ai/guides/) 

# Fix hot reload through a tunnel

Content updated October 25, 2026.

## Why doesn’t hot reload work when I open my dev server through a tunnel?

qURL™ carries hot reload over WebSockets and streaming responses over Server-Sent Events while your app stays on your machine with no public URL. Allow .qurl.site in Vite’s server.allowedHosts or the exact browser hostname in Next.js allowedDevOrigins, then restart. Cloudflare Quick Tunnels carry WebSockets but do not support Server-Sent Events; choose qURL when your app uses both.

## What you need

- A Vite or Next.js dev server opened through a tunnel
- The browser console open on the page that doesn’t update

## Steps

1. ### Tell the three failures apart  
A host rejection stops the page itself: Vite shows “Blocked request. This host is not allowed”, and next dev logs “Blocked cross-origin request to Next.js dev resource”. A WebSocket failure lets the page load, but edits never arrive, and the console shows the hot reload socket failing to connect. A Server-Sent Events problem leaves the page and hot reload working while a streamed response, such as a chat reply, stalls; in the Network tab its event-stream request never delivers events. Each has a different fix below.
2. ### Host rejected: let the dev server accept the tunnel’s hostname  
Vite answers “Blocked request” for an unknown host, and Next.js 16.2 and later block cross-origin requests to dev resources, including the hot reload socket. Add .qurl.site (or your tunnel’s domain) to Vite’s server.allowedHosts, or the exact browser hostname to Next.js allowedDevOrigins, and restart. The Next.js fix guide under Related Resources shows how to get that hostname from the recipient without consuming their link.
3. ### WebSocket failing: check the console for Vite’s fallback message  
If the WebSocket through the tunnel fails, Vite’s client falls back to connecting straight to the dev server, bypassing the tunnel, and logs “Direct websocket connection fallback.” That fallback only works on the machine running Vite, so a remote viewer gets no updates.
4. ### Use a tunnel that carries WebSockets, or point Vite at it  
Vite’s docs list three fixes: have the proxy carry WebSockets; or set server.strictPort to true and server.ws.clientPort to the same value as server.port; or give server.ws.port a different value from server.port. Vite 8.1 renamed server.hmr to server.ws and still syncs the old names. Through qURL the hot reload socket carries edits to the remote browser; allow the hostname first.  
```  
qurl publish http://127.0.0.1:5173
qurl share <CRID>
```
5. ### Stream stalling: check the tunnel carries Server-Sent Events  
This is not a hot reload problem: Vite’s hot reload uses a WebSocket, while chat replies, progress feeds and some dev tools stream over Server-Sent Events. Cloudflare Quick Tunnels do not support Server-Sent Events, so a stream that stalls there needs a different tool. Use qURL to carry the stream as each event is sent.

## What the recipient sees

With the socket connected, an edit to the source appears in the browser opening the qURL link without a manual refresh.

## Which tools carry hot reload to a remote browser?

| Option                                                                                  | Choose it when                                                                                                                                        | Trade-off                                                                                                                                                                                                                                        |
| --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [qURL](https://layerv.ai/research/qurl-behaviour/)                                      | qURL fits when a remote viewer needs live updates: hot reload and Server-Sent Events reach their browser while the app stays on your machine.         | For qURL, allow .qurl.site in Vite’s server.allowedHosts, or the exact browser hostname in Next.js allowedDevOrigins, then keep your computer and dev server running during the review.                                                          |
| [ngrok](https://layerv.ai/compare/qurl-vs-ngrok/)                                       | ngrok fits when you already use it: ngrok’s docs say WebSocket connections work with no configuration.                                                | ngrok needs your exact assigned dev domain in the dev server’s allowed hosts, an account, and a public URL.                                                                                                                                      |
| [Cloudflare Quick Tunnels](https://layerv.ai/compare/qurl-vs-cloudflare-quick-tunnels/) | A Cloudflare Quick Tunnel fits hot reload over a WebSocket with no account: Cloudflare’s Tunnel FAQ says Cloudflare Tunnel fully supports WebSockets. | Cloudflare Quick Tunnels do not support Server-Sent Events, so an app or dev tool that streams over SSE stalls. Cloudflare’s Sandbox tunnel comparison lists SSE support for named tunnels; the Quick Tunnel documentation lists no SSE support. |

## Troubleshooting

| If you see                                                                                                             | Do this                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| qurl publish says the local app is not accepting TCP connections                                                       | Start the app first and check that curl works on the same address. qurl publish connects to localhost as 127.0.0.1, so if your app listens only on IPv6 (on macOS or Linux, lsof -iTCP -sTCP:LISTEN -nP shows \[::1\]), publish 'http://\[::1\]:PORT' instead, or start the dev server with --host 127.0.0.1 and publish that address. Neither requires listening on your LAN. Keep the quotes: zsh treats the brackets as a pattern.                                                                                                         |
| The recipient sees “Access link invalid”                                                                               | The link was already used or has expired. Run qurl share again and send the new link.                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| The recipient sees “Resource not found”                                                                                | The resource was deleted after the link was made. Publish the app again, then run qurl share and send the new link.                                                                                                                                                                                                                                                                                                                                                                                                                           |
| publish rejects http://127.0.0.1 with “only HTTPS URLs are allowed”, or asks you to log in                             | You have a CLI older than 3.0\. On macOS run brew update, then brew upgrade qurl; elsewhere install the latest release again. After updating, qurl --version should print 3.4.1 or later.                                                                                                                                                                                                                                                                                                                                                     |
| publish prints no Access: line, or the Access: line says public and you did not ask for public                         | With no Access: line, the CLI is older than 3.4.1: on macOS run brew update, then brew upgrade qurl; elsewhere install the latest release again. A resource that an older CLI published is public unless you published it with --private, and anyone who has the CRID of a public resource can open it. To make a public resource private, run qurl delete <CRID> and publish again with qurl 3.4.1 or later. The new resource is private and has a new CRID.                                                                                 |
| publish says this Linux session “can’t reach a systemd user manager”                                                   | You are in a container or sandbox without a user service manager. Run qurl publish with --foreground; sharing lasts as long as that process runs.                                                                                                                                                                                                                                                                                                                                                                                             |
| publish says “Quota Exceeded” with “protected resource limit reached”, or “Your qURL account has reached a plan limit” | You can see this without an account: a device with no account can keep up to 3 active resources, with links that last up to 24 hours. Run qurl list --status active, then qurl delete <CRID> for any you no longer need (it asks you to confirm; --yes skips that). qurl stop keeps the resource, so it does not free a slot.                                                                                                                                                                                                                 |
| Updates work on your machine but not for the person you shared with                                                    | The Vite client fell back to a direct connection that only your machine can make. Check the console for the fallback message and use a tunnel that carries WebSockets.                                                                                                                                                                                                                                                                                                                                                                        |
| A tool that streams with Server-Sent Events stalls on a Cloudflare quick tunnel                                        | Quick Tunnels do not support Server-Sent Events, which is a separate limit from WebSockets. Use qURL to carry Server-Sent Events without buffering.                                                                                                                                                                                                                                                                                                                                                                                           |
| webpack-dev-server shows “Invalid Host header”                                                                         | Its host check rejected the tunnel. Add the domain to devServer.allowedHosts; a leading dot covers subdomains. Set client.webSocketURL to auto://0.0.0.0:0/ws too, or the hot reload client asks for the dev server’s port through the tunnel; the webpack-dev-server guide has the config. In Create React App, WDS\_SOCKET\_HOST and WDS\_SOCKET\_PORT set where the hot reload socket connects. Avoid allowedHosts: ‘all’ and DANGEROUSLY\_DISABLE\_HOST\_CHECK: both turn the check off, which webpack and Create React App warn against. |

## Frequently Asked Questions

Does qURL support WebSockets and hot reload? 

Yes. qURL carries WebSocket upgrades, Server-Sent Events and streamed responses. Add .qurl.site to Vite’s server.allowedHosts. In Next.js, set QURL\_DEV\_ORIGIN\_HOST to the recipient’s exact browser hostname for allowedDevOrigins and restart next dev.

Do Cloudflare quick tunnels support WebSockets? 

Yes. Quick Tunnels run on Cloudflare Tunnel, which Cloudflare’s Tunnel FAQ says fully supports WebSockets. They do not support Server-Sent Events, and they allow 200 in-flight requests.

Should I set server.hmr or server.ws? 

Either on Vite 8.1 and later, which renamed the WebSocket options to server.ws and keeps the old server.hmr names in sync. On earlier versions, use server.hmr.

Who else can reach my app while the link is open? 

Opening the link admits the network address it was opened from for the session, which lasts 1 hour by default. Other devices that share that public IP address, such as people on the same office or café network or VPN, can reach the app until the session ends. By default a link admits whoever opens it; access rules set through the API, such as IP allow and deny lists, can restrict that. Keep your app’s own sign-in for anything sensitive.

## Related Resources

[Guide: Share a streaming AI chatbot →](https://layerv.ai/guides/share-streaming-ai-demo/) [Guide: Fix Vite “Blocked request” →](https://layerv.ai/guides/fix-vite-blocked-request-host-not-allowed/) [Guide: Fix Next.js blocked dev requests →](https://layerv.ai/guides/fix-nextjs-blocked-cross-origin-dev-request/) [Guide: Allow a tunnel’s hostname in your dev server →](https://layerv.ai/guides/allow-tunnel-hostname-in-dev-server/) [Guide: Fix webpack-dev-server “Invalid Host header” →](https://layerv.ai/guides/fix-webpack-dev-server-invalid-host-header/) [How we tested qURL →](https://layerv.ai/research/qurl-behaviour/) [Vite docs: server options ↗](https://vite.dev/config/server-options)

## Publish your app now

Install the CLI, then run one command. No sign-up, no API key.

[Install the CLI ↗](https://layerv.ai/cli/)



---

*This markdown version is auto-generated from [https://layerv.ai/guides/fix-hot-reload-through-a-tunnel/](https://layerv.ai/guides/fix-hot-reload-through-a-tunnel/) for AI agents. Curated agent resources: [llms.txt](https://layerv.ai/llms.txt). For the full interactive experience, visit the HTML version.*

