Node Watch Mode in Staging: Rapid Loops Without Leaking Watchers

Node Watch Mode in Staging: Rapid Loops Without Leaking Watchers

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 with exclusive: true or systemd/ECS single-task constraints; never enable --watch in 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

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply