Why Your Nodemon Restarts Feel Flaky — and How to Fix Them
Flaky nodemon restarts usually come from atomic editor writes, missing debounce, and bad ignore globs. Here's how to fix all three with chokidar, delay, and correct patterns.
22 Jan 2026, 10:26 UTC

You hit save in VS Code, nodemon restarts. You hit save again, nothing happens. You save a third time and it restarts twice in a row. If this sounds familiar, the problem usually isn't nodemon being broken — it's the interaction between your editor's atomic writes, nodemon's default watch backend, and a missing debounce delay. All three are configurable, and the fixes take about five minutes.
This post covers the three knobs that matter most: the watch backend, ignore patterns, and restart delay. Examples assume nodemon 3.x, which requires Node 16 or later. Check yours with nodemon --version.
The watch backend: legacy polling vs chokidar
Nodemon ships with two file-watching strategies. The default, kept for backward compatibility, is the legacy watcher built on Node's fs.watch/fs.watchFile. The alternative is chokidar, a mature watching library that normalizes events across platforms.
The legacy watcher's weak spot is atomic writes. Many editors — VS Code, WebStorm, Vim with certain settings — don't overwrite your file directly. They write to a temporary file and rename it over the original. From the OS's perspective, the file you were watching was replaced, and fs.watch can silently lose track of it. That's the "saved but nothing happened" symptom.
Chokidar handles rename-and-replace reliably, so for most new projects it's the better default:
nodemon --legacy-watch=false server.js
Or in nodemon.json:
{
"legacyWatch": false,
"watch": ["src"],
"ext": "js,json"
}
The trade-off: chokidar adds roughly 2 MB of install size and pulls in an optional native dependency (fsevents) on macOS. In size-constrained containers or offline installs where optional native builds fail, legacy watch may be the pragmatic choice. One exception worth knowing: on Windows with network-mounted drives, legacy polling is often more stable than chokidar. Test on your actual setup rather than assuming.
Ignore patterns that actually match
Ignore patterns are matched against absolute paths, which trips people up. Writing "ignore": ["node_modules/"] looks right but misses nested copies of node_modules deeper in the tree. The safe form uses a leading **/:
{
"ignore": [
"**/node_modules/**",
"**/*.test.js",
"**/coverage/**"
]
}
Verify it by editing a test file while nodemon runs — you should see no restart. One cross-platform caveat: matching is case-sensitive on Linux and macOS but case-insensitive on Windows. If your team mixes operating systems, keep directory casing consistent or your ignores will behave differently per machine.
Also note that nodemon only watches directories it was told about. Files created later in an unwatched directory won't trigger restarts until you restart nodemon itself — a common source of "why didn't it pick up my new route file" confusion.
Delay: one restart instead of five
The --delay option debounces restarts. Without it, a tool that writes several files in quick succession — TypeScript's compiler, a code generator, even git checkout — triggers a restart per change batch, and your app thrashes. With a delay, nodemon waits until changes stop for that window and restarts once:
nodemon --delay 300 server.js
Values between 100 and 500 ms work well in practice. Understand what delay does not do: it doesn't queue restarts. A burst of twenty changes inside the window produces exactly one restart after the last change. That's usually what you want, but if your workflow depends on seeing every intermediate state, delay will hide it.
A worked example: TypeScript without a build step
Combining these options with a custom exec command gives you a zero-build TypeScript dev loop. Nodemon normally runs node, but exec replaces the command while --ext widens what's watched:
{
"legacyWatch": false,
"watch": ["src"],
"ext": "ts,json",
"ignore": ["**/*.test.ts", "**/node_modules/**"],
"delay": 300,
"exec": "node -r ts-node/register src/server.ts"
}
Run it with plain nodemon from the project root — no arguments needed, since the config file supplies everything. Config precedence is CLI flags over nodemon.json over the nodemonConfig key in package.json, so a one-off nodemon --delay 100 temporarily overrides the file without editing it.
One signal-handling note if you go further and wrap nodemon in npm scripts: prefer a direct node invocation in exec over shell chains like npm run build && node dist. Nodemon forwards SIGINT/SIGTERM to its child and force-kills after a timeout, but extra shell layers can swallow those signals and leave orphaned processes holding your port.
Where this falls short
Nodemon restarts the whole process on every change. For a small API that's fine; for an app with a 10-second boot, no amount of delay tuning fixes the fundamental cost. At that point, Node's built-in node --watch (stable since Node 20) or a hot-reloading setup is worth evaluating. Nodemon's remaining edge is exactly what this post covered: mature ignore globs, debounce, custom exec, and predictable signal forwarding.
Actionable next step: flip legacyWatch to false, add a 300 ms delay, fix your ignore globs with leading **/, then edit a file rapidly five times in a row. You should see exactly one clean restart. If you don't, the watch backend is the first thing to swap back and re-test.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.