node --watch is fantastic for staging feedback loops and catastrophic when a restart leaves the previous process bound to :3000, file watchers accumulating under /app, or health checks flapping because two listeners answer alternately. The unfair advantage is treating watch mode like a supervised child: exclusive port lock, graceful close on restart, and watcher scopes that ignore node_modules and build artifacts.
⚡ TL;DR: Use
node --watch --watch-path=./src(not the whole monorepo); on restart, close HTTP servers and clear intervals before exit; bind withexclusive: trueor systemd/ECS single-task constraints; never enable--watchin production images. Pair with Graceful Shutdown for Node and Node Module Compile Cache.
Scope what you watch
// package.json (staging scripts only)
{
"scripts": {
"dev:staging": "node --watch --watch-path=./src --watch-path=./config ./dist/server.js",
"start": "node ./dist/server.js"
}
}
# ❌ Watching the entire workspace — rebuilds thrash on every log write
node --watch .
Close the previous listener on reload
Watch restarts send signals; your process must drain.
// src/server.ts
import { createServer } from "node:http";
const server = createServer((req, res) => { /* ... */ res.end("ok"); });
async function listen() {
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
// ✅ exclusive avoids silent dual-bind on some platforms
server.listen({ port: Number(process.env.PORT ?? 3000), host: "0.0.0.0", exclusive: true }, () => resolve());
});
}
async function shutdown(signal: string) {
console.log(JSON.stringify({ msg: "shutdown", signal }));
await new Promise<void>((resolve) => server.close(() => resolve()));
// clear intervals, close DB pools, abort AbortControllers
process.exit(0);
}
process.on("SIGTERM", () => void shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));
await listen();
Staging container hygiene
| Risk | Mitigation |
|---|---|
| Dual bind / EADDRINUSE | exclusive: true + one task per sandbox |
| Watcher FD leak | --watch-path narrow; ignore uploads/ |
| Zombie children | Don’t spawn unmanaged child_process without kill-on-exit |
| Prod footgun | Strip --watch from prod CMD; CI grep forbids it |
| Health flap | preStop sleep + readiness fail during restart |
# ✅ Production image — no watch
CMD ["node", "dist/server.js"]
# Staging override in compose:
# command: ["node","--watch","--watch-path=./src","dist/server.js"]
// ❌ setInterval without clear on shutdown — leaks across watch restarts in same PID when watch reuses process incorrectly
setInterval(() => poll(), 1000);
Closing checklist
✅ Dos
– ✅ Narrow --watch-path to source + config only
– ✅ Implement SIGTERM/SIGINT close for HTTP + pools
– ✅ Keep watch out of production Docker CMD
– ✅ Use exclusive listen or single-replica staging tasks
– ✅ Grep CI for --watch in prod entrypoints
❌ Don’ts
– ❌ Don’t watch node_modules, coverage, or log dirs
– ❌ Don’t ignore EADDRINUSE in staging “because refresh”
– ❌ Don’t leave intervals/sockets open across restarts
– ❌ Don’t enable watch on multi-replica ECS services
Related reading
- Graceful Shutdown for Node: Drain Correctly on Kubernetes and ECS
- Node Module Compile Cache: Faster Cold Starts for Heavy CLIs
- Node Permission Model: Lock Down Production Processes Running Plugins
- Node Test Runner Shards: Parallel CI Without Introducing Flakes
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
