5 min read
On this page

File Watchers & Hot Reload

The Feedback Loop

The most important variable in developer productivity is the length of your feedback loop. The feedback loop is the time between making a change and seeing the result of that change.

Change code -> [feedback loop] -> See result

When this loop takes 30 seconds, you stay in flow. Your thought process is: change, verify, change, verify — a tight cycle of hypothesis and observation.

When this loop takes 5 minutes, you check Slack. You open a browser tab. You lose your mental model. By the time the result appears, you've forgotten exactly what you were testing.

File watchers and hot reload shrink this loop to near zero. You save a file, and the result appears instantly — tests re-run, the browser updates, the server restarts. The feedback is so fast that it feels like the code is alive, responding to your edits in real time.

This isn't a luxury. It's a fundamental requirement for productive development. If your feedback loop is measured in minutes, fixing it is the highest-leverage productivity improvement available to you.

How File Watchers Work

A file watcher monitors a directory for changes and triggers an action when a file is created, modified, or deleted.

At the operating system level, this uses platform-specific APIs:

Linux:   inotify
macOS:   FSEvents
Windows: ReadDirectoryChangesW

Most tools abstract over these differences so you don't need to think about them. You tell the tool what to watch and what to do, and it handles the rest.

Basic file watching with common tools

nodemon (Node.js):

# Restart server on file changes
nodemon src/server.ts

# Watch specific extensions
nodemon --ext ts,json src/server.ts

# Ignore specific directories
nodemon --ignore dist --ignore node_modules src/server.ts

watchexec (language-agnostic):

# Run tests when any .go file changes
watchexec -e go -- go test ./...

# Run a build when source files change
watchexec -e rs -- cargo build

# Run any command on file change
watchexec -e py -- python -m pytest tests/

entr (Unix, minimal):

# Run tests when files change
find . -name "*.go" | entr go test ./...

# Restart a server when source changes
find . -name "*.py" | entr -r python app.py

# Run a single command on any change
ls *.md | entr make docs

chokidar (Node.js, programmatic):

# In a script or build tool
const chokidar = require('chokidar');

chokidar.watch('src/**/*.ts').on('change', (path) => {
  console.log(`File changed: ${path}`);
  // Run tests, rebuild, etc.
});

Hot Reload vs Hot Restart vs Full Rebuild

These terms are often confused. They represent three different approaches to updating a running application after a code change.

Full rebuild

Stop the process, rebuild everything from scratch, start the process again.

Change file -> Stop server -> Build everything -> Start server -> Navigate to page
Feedback time: 10-60 seconds

This is the slowest option and should be your last resort. Unfortunately, it's the default for many setups.

Hot restart

Detect the change, restart only the affected process, preserve some state.

Change file -> Restart server process -> Reconnect
Feedback time: 1-5 seconds

Tools like nodemon and air (Go) do this. The server restarts, but the restart is fast because there's no full rebuild. Some state is lost (in-memory data, open connections), but for most development work this is acceptable.

Hot module replacement (HMR)

Replace only the changed module in the running application without restarting. Application state is preserved.

Change file -> Replace module in memory -> UI updates
Feedback time: 50-500 milliseconds

This is the gold standard for frontend development. When you change a React component, only that component re-renders. Your application state — form inputs, scroll position, navigation history — is preserved.

# Vite (default behavior, no configuration needed)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
# Edit any file — changes appear in the browser instantly
# Webpack with HMR
// webpack.config.js
module.exports = {
  devServer: {
    hot: true,
  },
};

Setting Up Fast Feedback Loops by Language

JavaScript/TypeScript

Frontend development has the best hot reload story thanks to modern bundlers.

# Vite (recommended for new projects)
npm run dev
# HMR is automatic. Changes appear in ~50ms.

# Next.js
npm run dev
# Fast Refresh handles component updates automatically.

# For server-side Node.js
# Use --watch flag (Node 18+)
node --watch src/server.ts

# Or use tsx for TypeScript
npx tsx watch src/server.ts

Go

# air: hot restart for Go applications
# Install
go install github.com/air-verse/air@latest

# Initialize config
air init

# Run with file watching
air

# .air.toml configuration
# [build]
#   cmd = "go build -o ./tmp/main ."
#   bin = "tmp/main"
#   include_ext = ["go", "html", "tmpl"]
#   exclude_dir = ["tmp", "vendor"]

Python

# Flask/Django have built-in reload
flask run --debug           # Flask with auto-reload
python manage.py runserver  # Django with auto-reload

# For general Python scripts
watchexec -e py -- python my_script.py

# pytest-watch for test automation
pip install pytest-watch
ptw    # Reruns tests on every .py file change

Rust

# cargo-watch
cargo install cargo-watch

# Recompile on changes
cargo watch -x build

# Run tests on changes
cargo watch -x test

# Run the application on changes
cargo watch -x run

# Chain commands
cargo watch -x "check" -x "test" -x "run"

Test Re-Running on File Change

Automatically re-running tests when files change is the highest-value file watcher setup for most engineers. It turns "run tests" from a conscious decision into an automatic background process.

JavaScript/TypeScript

# Vitest (watch mode is default)
npx vitest

# Jest
npx jest --watch

# Jest with specific file filtering
npx jest --watch --testPathPattern="user"

Go

# Using watchexec
watchexec -e go -- go test ./...

# Using gotestsum for better output
watchexec -e go -- gotestsum --format short ./...

# Watch specific package
watchexec -e go -w internal/auth -- go test ./internal/auth/...

Python

# pytest-watch
ptw -- -x --tb=short

# -x: stop on first failure
# --tb=short: compact tracebacks

The key configuration decisions

What to watch. Only watch source files, not build output, node_modules, or generated files. Watching too broadly causes infinite loops (change triggers build, build output triggers another build).

# Good: watch only source
watchexec -e go -w src/ -w internal/ -- go test ./...

# Bad: watch everything (will trigger on build output)
watchexec -- go test ./...

What to ignore. Explicitly ignore directories that change during builds:

# nodemon
nodemon --ignore dist --ignore coverage --ignore node_modules

# watchexec
watchexec --ignore dist --ignore target -- cargo test

Debouncing. When you save a file, your editor might write it multiple times (save, format, save again). Debouncing waits a short period after the last change before triggering the action, preventing unnecessary double-runs.

# watchexec has built-in debouncing (default 100ms)
watchexec --debounce 300 -e go -- go test ./...

Building Sub-Second Feedback Loops

The target is under 1 second from save to result. Here's how to get there:

Run only affected tests

Running the entire test suite on every change is wasteful. Most changes affect a small number of tests.

# Vitest: runs only tests related to changed files
npx vitest

# Jest: same behavior in watch mode
npx jest --watch

# Go: watch specific packages
watchexec -e go -w internal/auth -- go test ./internal/auth/...

Incremental compilation & parallel tests

Languages with incremental compilation (Go, Rust, TypeScript) only recompile changed files. Enable incremental mode in TypeScript via tsconfig.json. Rust uses incremental builds by default in debug mode. Combine this with parallel test execution (Go runs parallel by default, Vitest uses worker threads, pytest has xdist) and keep your dev server running rather than restarting from scratch on each change.

Common Pitfalls

Infinite loops from watching build output. If your watcher triggers on changes in the dist/ directory, and the build writes to dist/, you get an infinite loop. Always exclude build output directories.

Watching too many files. On large projects, watching every file can consume significant resources and cause slow trigger times. Narrow the watch to source directories only.

Not debouncing. Without debouncing, a single save might trigger multiple rebuilds if the editor writes the file in multiple steps (format-on-save, for example). Most tools handle this by default, but verify if you see double-runs.

Ignoring test isolation. If tests share state (database, files, global variables), running them in parallel or in watch mode can produce flaky results. Fix the tests, don't disable the watcher.

Slow tests undermining the fast loop. If your test suite takes 30 seconds, re-running it on every save is counterproductive. Use the watcher to run only affected tests, and run the full suite before committing.

Over-investing in hot reload for rarely-changed code. Spending 4 hours setting up perfect HMR for a configuration page you edit once a month is not a good trade. Focus your feedback loop optimization on the code you change most frequently.

Key Takeaways

The feedback loop — time from code change to seeing the result — is the most important variable in daily development productivity. Aim for under 1 second.

File watchers automate the trigger: save a file, and the tests/build/server update automatically. This eliminates the manual "run tests" step and keeps you in flow.

Hot module replacement (HMR) is the fastest option for frontend work. Hot restart (nodemon, air) is good enough for backend services. Full rebuild should be your last resort.

Configure watchers to watch only source files, ignore build output, and debounce to prevent double-triggering. Run only affected tests, not the entire suite, on each change.

Every language and framework has file watching tools. The investment to set them up is usually under an hour. The daily time savings last for the life of the project.