How to Use cargo watch for Auto-Recompilation

Install `cargo-watch` via Cargo or your system package manager, then run `cargo watch -x run` in your project root to automatically recompile and execute your binary whenever source files change.

The edit-compile-run loop is stealing your time

You fix a typo in your logic. You hit save. Now you switch windows, kill the running process, type the build command again, wait for the compiler, and hope you didn't mistype the flags. You do this forty times a day. That is forty minutes of your life spent on mechanical repetition instead of thinking.

cargo-watch reclaims that time. It monitors your source files and restarts your program the moment you save. You type, you save, the code runs. The loop shrinks from seconds to milliseconds. You stay in the flow. The tool handles the busywork.

How file watching actually works

Rust's standard library does not expose file system watching directly. The mechanisms vary wildly across operating systems. Linux uses inotify. macOS uses FSEvents. Windows uses ReadDirectoryChangesW. Each API has different limits, different event types, and different quirks.

cargo-watch wraps these platform differences behind a single interface. It registers interest in specific paths. When the OS detects a write event, it signals the watcher. The watcher then decides whether to trigger an action.

Think of it like a smoke detector. The detector does not check the air every millisecond. It waits for the sensor to trip. cargo-watch listens for the OS to shout "File changed!" and then it reacts. The tool does not poll the disk. Polling wastes CPU and misses rapid changes. Event-driven watching is efficient and immediate.

The OS does the heavy lifting. cargo-watch just listens and reacts.

Minimal setup

Install the tool once. Run it whenever you start a development session.

cargo install cargo-watch

Navigate to your project root. Start the watcher with the -x flag to specify the command.

cargo watch -x run

The -x flag tells the tool to execute the following argument as a command. Here, it runs cargo run. The tool spawns cargo run as a child process. It also sets up watchers on the src directory and Cargo.toml.

Save a file. The terminal clears. The program restarts.

Run this once. Save a file. Watch it go.

The lifecycle of a change

Understanding what happens under the hood prevents frustration. When you edit a file, your editor does not just write bytes to disk. Modern editors use atomic writes. They write to a temporary file, rename it to the target name, and update metadata. This sequence generates multiple file system events.

If the watcher reacted to every event, your program would restart three times for one keystroke. The first restart would happen while the file is still being written. The second would catch a partial write. The third would see the final state. This creates a race condition where your program crashes or reads garbage.

cargo-watch uses a debounce timer to solve this. When the first event arrives, the tool starts a short timer, usually 200 milliseconds. If more events arrive before the timer expires, the timer resets. The tool waits for a burst of events to finish. Once the silence holds, it assumes the change is complete.

After the debounce window closes, the tool sends a termination signal to the child process. It waits for the process to exit. Once the process is gone, it spawns the command again.

Debounce is not a bug. It is the only reason the tool works with modern editors.

Realistic configuration

The default behavior watches everything in the current directory. This works for small projects. Large projects generate noise. The compiler writes thousands of files to the target directory. If you watch target, the watcher triggers on every compilation step. This creates a feedback loop or just floods the terminal with restarts.

Create a .cargo-watch.toml file in your project root to control behavior. This file lives in your repository. Your teammates get the same behavior automatically.

[watch]
# Ignore directories that generate noise.
# The compiler writes heavily to target/.
# VCS directories change on every commit.
ignore = ["target", "vendor", ".git"]

# Only care about Rust source and manifest files.
# This reduces false positives from IDE temp files.
filter_files = ["*.rs", "Cargo.toml", "Cargo.lock"]

[run]
# The command to execute on change.
command = "cargo run"

# Clear the terminal screen before each run.
# Keeps the output readable.
clear = true

The ignore list is essential. target must be ignored. vendor must be ignored if you use vendored dependencies. .git must be ignored because version control tools update metadata constantly.

The filter_files list adds precision. Some IDEs write temporary files with random extensions. Filtering by *.rs ensures only relevant changes trigger a restart.

Commit the config file. Your teammates will thank you when their watchers stop screaming over target/.

Advanced workflows

cargo-watch is not limited to running binaries. It runs any command. This unlocks powerful workflows.

Running tests automatically

Test-driven development thrives on fast feedback. You write a test. You save. You want to see it fail immediately. You fix the code. You save. You want to see it pass.

cargo watch -x test

This runs your test suite on every change. For small projects, this is instant. For large projects, it might be slow. Use filtering to run only relevant tests.

cargo watch -x "test --lib"

This runs only library tests, skipping integration tests if they are slow. You can also watch specific directories.

cargo watch -w src/lib.rs -x "test --lib"

The -w flag restricts watching to specific paths. This is useful when you are working on one module and do not want to trigger a full test run when you touch unrelated files.

Passing arguments and environment variables

You often need to pass arguments to your binary or set environment variables. The syntax requires care.

RUST_LOG=debug cargo watch -x "run -- --verbose"

The RUST_LOG=debug part sets the environment variable for the watcher process. The watcher inherits this environment when it spawns the child.

The run -- --verbose part passes arguments to cargo run. The first -- separates cargo arguments from binary arguments. cargo-watch sees run as the command. It passes --verbose to the binary.

If you need to pass arguments to cargo-watch itself, they must come before the -x flag.

cargo watch -w src -x "run -- --port 8080"

Here, -w src tells the watcher to watch only the src directory. The rest is the command.

The shell trap

By default, cargo-watch executes commands directly. It does not invoke a shell. This is faster and safer. It avoids shell injection issues and platform-specific shell differences.

If you need shell features like pipes, redirects, or variable expansion, you must request a shell explicitly.

cargo watch -x "run | tee output.log"

This fails because there is no shell to interpret the pipe. You need the --shell flag.

cargo watch --shell -x "run | tee output.log"

Now the tool invokes /bin/sh (or cmd.exe on Windows) to run the command. The pipe works. The output goes to both the terminal and the log file.

Use --shell sparingly. Direct execution is preferred. Shell invocation adds overhead and introduces platform dependencies.

Pitfalls and process management

The tool manages processes. Process management introduces edge cases.

Zombie processes

cargo-watch waits for the child process to exit before restarting. If your application catches the termination signal and does not exit, the watcher hangs. It sits there waiting. You have to Ctrl+C the watcher to break the loop.

This happens when your app has a signal handler that swallows SIGTERM. Or when your app is stuck in a blocking call that ignores signals.

The solution is to ensure your app exits cleanly. If you cannot fix the app, use the --kill flag. This forces the watcher to send SIGKILL after a timeout.

cargo watch --kill -x run

This is a sledgehammer. It terminates the process immediately. Use it only when graceful shutdown fails.

File locks

Some applications hold file locks or database connections. If the process crashes or is killed abruptly, the lock might persist. The new instance fails to acquire the lock and crashes immediately. The watcher restarts it. It crashes again. You get a restart loop.

Check your application's resource cleanup. Ensure locks are released on exit. Use connection pooling with proper teardown. If the loop starts, kill the watcher and investigate the lock.

IDE conflicts

Integrated development environments often write to project directories. They might update metadata files, generate caches, or write temporary files. If the watcher sees these changes, it restarts unnecessarily.

Configure your IDE to write caches outside the project root. Add IDE-specific directories to the ignore list in .cargo-watch.toml. Common offenders include .idea, .vscode, and .metadata.

If the watcher freezes, your process is lying about being dead. Kill it manually and check your signal handlers.

Decision matrix

Choose the right tool for your workflow. Do not over-engineer the restart loop.

Use cargo-watch when you want a zero-config drop-in replacement for cargo run that understands Rust project structure and integrates with the Cargo ecosystem.

Use entr when you need to watch arbitrary files and pipe them to any command, regardless of language or build system, and you prefer a simple Unix pipe interface.

Use your IDE's built-in runner when you prefer a graphical interface, integrated debugging sessions, and do not want to manage terminal processes.

Use watchexec when you need advanced scripting capabilities, complex event filtering, and cross-platform consistency beyond Rust projects.

Pick the tool that matches your workflow. The goal is to spend time coding, not configuring the restart loop.

Where to go next