When Hot Reload Goes Cold
The problem
At some point in the last few weeks, my local development servers that watch for
file changes (e.g., almost every project that has an npm [run] start command)
have been failing to detect changed files until after some considerable delay —
in some cases, as long as 5 minutes’ delay.
As Laurie Bream would say, this is untenable.
The non-solutions
I tried a bunch of things that seemed like possible fixes, based on my Googling:
- Make sure the files being watched don’t include
node_modulesor.venvor__pycache__, etc. - Tightly scope the watched files to a very small subset of the repo, excluding configurations and package meta-files.
- Check active disk usage to see if something like a background virus scan, or other I/O-intensive task, is not being undertaken.
- A handful of other things that I honestly forget at this point.
One thing I tried did seem to work for some projects (but not all): Change the watch mode from filesystem events to polling.
The workaround
I got tired of trying to get the watch tools that were part of each ecosystem
to work. I looked for a bigger hammer.
Enter watchexec, a standalone tool
that watches for filesystem changes and can execute arbitrary shell commands or
invoke binaries when changes are detected - and it also supports a polling mode.
For this blog, built using Astro / AstroPaper, the normal dev command as
defined in package.json is:
{
"name": "astro-paper",
"type": "module",
"version": "5.5.0",
"scripts": {
"dev": "astro dev",
...
With watchexec, I set up a dev:watch command (broken into multi-line for
readability):
"dev:watch": "watchexec
--poll 5000ms
-w src
-w astro.config.ts
-w package.json
-w tsconfig.json
-e md,mdx,ts,astro,json
--restart
--stop-signal SIGTERM
--print-events
--no-vcs-ignore
-- pnpm run dev",
Let’s go through that invocation of watchexec one flag at a time:
| Flag | Purpose |
|---|---|
--poll 5000ms | Use polling mode instead of filesystem events. Check for file changes every 5 seconds (5000 milliseconds). This is the core workaround for dead watch mode. |
-w src | Watch the src/ directory for changes. |
-w astro.config.ts | Watch the Astro config file. Changes to build config should trigger a restart. |
-w package.json | Watch dependencies and script definitions. |
-w tsconfig.json | Watch TypeScript configuration. |
-e md,mdx,ts,astro,json | Only trigger on changes to these file types (markdown, MDX, TypeScript, Astro, JSON). Ignores unrelated changes like build artifacts. |
--restart | Kill the previous process before starting a new one. Ensures clean state. |
--stop-signal SIGTERM | Send SIGTERM (graceful shutdown) to the process instead of SIGKILL. Allows the dev server to clean up. |
--print-events | Log which files changed and triggered the restart. Helpful for debugging watch behavior. |
--no-vcs-ignore | Don’t skip files ignored by .gitignore. By default, watchexec respects .gitignore, but explicitly watching files with the -w flag overrides this anyway. |
-- pnpm run dev | The command to execute. Everything after -- is passed as the command to run. |
Here’s the full scripts section of my package.json:
{
"scripts": {
"dev": "astro dev",
"dev:watch": "watchexec --poll 5000ms -w src -w astro.config.ts -w package.json -w tsconfig.json -e md,mdx,ts,astro,json --restart --stop-signal SIGTERM --print-events --no-vcs-ignore -- pnpm run dev",
"build": "astro check && astro build && pagefind --site dist && cp -r dist/pagefind public/",
"preview": "astro preview",
"sync": "astro sync",
"astro": "astro",
"format:check": "prettier --check .",
"format": "prettier --write .",
"lint": "eslint .",
"new-post": "node scripts/new-post.js"
}
}
If you’re encountering the same problem and you’re just about done wasting time
on it, check out watchexec: https://github.com/watchexec/watchexec
Fixing the underlying problem
This doesn’t fix the underlying problem of my dev servers no longer triggering due to filesystem events, but it does unblock me and get my stuff working without needing to put further time into figuring something out that isn’t really that big of a deal.
If you’re encountering a similar issue on MacOS and you find an actual solution, please share your findings! I’ve already spent too much time trying to fix the problem and I’m satisfied with my workaround for now, but it does itch at me that I have to use a workaround because I can’t figure out the issue.
Here is some diagnostic information:
Machine: MacBook Pro M4 Max
OS: MacOS Version 15.1 (24B2083)
I have recently been plugging / unplugging external hard drives, formatting
them, moving files around between them, making disk images of them, writing disk
images to them, and just in general doing a lot of hard drive stuff with
external drives. I think maybe at some point in this process, I disabled or
otherwise broke the filesystem events that are dispatched when files get
changed.