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 onglobalThis, 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'sBun.serve()call doesn't collide with the last one still holding the port.globalThis.reloadCountpersists across reloads specifically because it's read offglobalThisrather than declared as a fresh locallet count = 0— a local would reset to0on every single save.
Gotchas
--hotis 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
globalThisresets on every reload, since the module itself is re-evaluated from scratch — aletat module scope behaves like a fresh variable each time, not a persisted one. --watch(hard restart) and--hot(soft reload) solve different problems —--watchis simpler and always correct, but loses all in-memory state and existing connections on every change; reach for--hotspecifically 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.