localhost:5173 / vite-hmr

Hot Module Replacement on localhost:5173

HMR swaps a changed module in the running page over a WebSocket instead of reloading it; when that WebSocket cannot connect, Vite falls back to full page reloads or stops updating at all.

What actually happens

Vite watches your files. On a change it sends the new module over a WebSocket to the browser, where it replaces the old one in place — component state survives, scroll position survives, the page does not reload. A full reload means that handoff failed somewhere.

The deprecated option almost every answer still shows

The WebSocket settings under server.hmr — protocol, host, port, path, clientPort, timeout, server — are deprecated. They live under server.ws now. The old keys are still synced automatically so existing configs keep working, which is exactly why the outdated advice never stopped circulating.

vite.config.js
// deprecated
server: { hmr: { clientPort: 443 } }

// current
server: { ws: { clientPort: 443 } }

Behind a reverse proxy or tunnel

The browser loads the page from your public hostname on port 443, then tries to open the WebSocket on the port Vite thinks it is on. clientPort tells the client where to connect instead.

vite.config.js
export default defineConfig({
  server: {
    ws: {
      protocol: 'wss',
      clientPort: 443,
    },
  },
})

The fallback message in the console

If the proxy in front of Vite does not forward WebSocket connections, the client gives up on the proxied route and connects to the HMR server directly. It prints a connection error first and then works anyway.

console
Direct websocket connection fallback. Check out https://vite.dev/config/server-options.html#server-ws to remove the previous connection error.

server.hmr today: on, off, and the overlay

What remains under server.hmr is the behaviour itself. Its type is boolean or an object with a single key, overlay. false turns HMR off; overlay: false hides the full-screen error overlay without touching the updates.

vite.config.js
server: {
  hmr: { overlay: false },
}

What does server.hmr.overlay actually show?

Errors the dev server sends over the HMR socket — a module that fails to transform, a plugin that throws. Not runtime errors from your own code in the browser; those stay in the console as before. The overlay names its own off switch, so the option name in your config is the one printed in the tip line.

browser
[plugin:vite:import-analysis] Failed to resolve import "./Foo" from "src/App.jsx". Does the file exist?
Click outside, press Esc key, or fix the code to dismiss.
You can also disable this overlay by setting server.hmr.overlay to false in vite.config.js.

With the overlay off, the error moves to the console

overlay: false does not swallow the error. The client logs it instead, as "[vite] Internal Server Error" followed by message and stack. The page keeps whatever it rendered last, which looks like a stale page rather than a broken one — the reason to switch the overlay off only when something else already shows these errors.

console
[vite] Internal Server Error
Failed to resolve import "./Foo" from "src/App.jsx". Does the file exist?

Why the page reloads after you fix the error

If the overlay was already showing when the page loaded, the first update after the fix triggers a full reload instead of a hot update — the page never finished loading, so there is no module graph to patch. Later errors are cleared in place: the next successful update removes the overlay without a reload.

Nothing updates at all

Two usual causes, both outside Vite. On a bind-mounted volume in Docker or on some network drives, filesystem events never reach the watcher — polling is the workaround. And a module that does not accept an update propagates upward until something does; if nothing does, Vite reloads the page. That is not a bug, that is the fallback working.

vite.config.js
server: {
  watch: { usePolling: true },   // only when events do not arrive
}

Turning it off is a cost

usePolling asks the filesystem for changes on a timer instead of being told. On a large project it keeps a core busy. Use it where events genuinely do not arrive, not as a default.

# faq

Questions

What does HMR stand for?

Hot Module Replacement — replacing a changed module in the running page without reloading it.

Why does my page do a full reload instead of hot updating?

Either the WebSocket did not connect, or no module in the chain accepted the update, so Vite fell back to reloading.

Is server.hmr.clientPort still correct?

It still works but is deprecated. The current location is server.ws.clientPort.

How do I turn off the Vite error overlay?

Set server.hmr.overlay to false. HMR keeps working; server errors are then logged in the browser console as "[vite] Internal Server Error" instead.

Why does HMR not work in Docker?

File change events often do not cross a bind mount. server.watch.usePolling is the usual workaround, at the cost of CPU.

# next

Related