10LOC
Bunintermediate

Hot-reloading a dev server with bun --hot

Published February 5, 2027

declare global {
  var server: ReturnType<typeof Bun.serve> | undefined;
  var reloadCount: number;
}

globalThis.reloadCount = (globalThis.reloadCount ?? 0) + 1;
globalThis.server?.stop();

globalThis.server = Bun.serve({
  port: 3000,
  fetch: () => new Response(`Reloaded ${globalThis.reloadCount} times`),
});

console.log(`Reload #${globalThis.reloadCount}, listening on ${globalThis.server.url}`);

export {};

What

bun --hot server.ts watches every file the entrypoint imports and, on change, re-evaluates the code in the same process instead of restarting it. globalThis survives across reloads, so state — like a running server — can persist deliberately instead of resetting on every save.

Why it matters

nodemon-style watching restarts the whole process on every change, which means every open connection drops and any in-memory state (caches, in-flight requests, a WebSocket room) resets. bun --hot re-transpiles and re-evaluates the changed module graph without tearing down the process, so a server can keep the same port bound and the same in-memory state warm across edits — the tradeoff is that you now own deciding what should reset on reload and what shouldn't, since nothing does so automatically.

How it works

  • declare global { var server; var reloadCount } tells TypeScript these will exist on globalThis, since assigning arbitrary properties to it isn't type-safe by default.
  • Every reload re-runs this whole module top-to-bottom, including the Bun.serve() call — so without guarding against it, each save would attempt to start a second server.
  • globalThis.server?.stop() shuts down the previous reload's server (if one exists) before starting a fresh one, so this reload's Bun.serve() call doesn't collide with the last one still holding the port.
  • globalThis.reloadCount persists across reloads specifically because it's read off globalThis rather than declared as a fresh local let count = 0 — a local would reset to 0 on every single save.

Gotchas

  • --hot is not the same as browser-side hot module replacement (Vite-style component reloading) — it's the server-side equivalent: your route handlers update, but nothing pushes a change to a connected browser tab automatically.
  • Anything not read from globalThis resets on every reload, since the module itself is re-evaluated from scratch — a let at module scope behaves like a fresh variable each time, not a persisted one.
  • --watch (hard restart) and --hot (soft reload) solve different problems — --watch is simpler and always correct, but loses all in-memory state and existing connections on every change; reach for --hot specifically when that cost is the thing you're trying to avoid.

Related

  • server.reload({ routes, fetch }) — swaps a running server's handlers explicitly, useful for hot-reloading route tables without relying on --hot's automatic module re-evaluation at all.
  • bun --watch — the simpler, hard-restart sibling; better default when a process doesn't hold meaningful in-memory state worth preserving across a reload.