How to Fix "cargo build slow" — Speeding Up Builds

Fix slow cargo builds by clearing the target directory and avoiding blanket hint-mostly-unused profiles.

The build loop that won't end

You type cargo build. The terminal stares back. You go to get coffee. You return. The progress bar is still crawling. You change a single variable name in your source file and hit build again. The progress bar resets. You wait again. The build time hasn't dropped.

Rust's build times have a reputation. The reputation is earned. Compiling Rust involves running LLVM, a heavy optimization engine that generates machine code from scratch. Every dependency in your graph goes through the same process. If Cargo recompiles everything every time you save, the feedback loop breaks. You stop experimenting. You start guessing.

Speeding up builds isn't about magic flags. It's about understanding what Cargo caches, what breaks the cache, and how to keep the incremental compiler happy.

How Cargo tracks work

Cargo doesn't just run rustc. It manages a dependency graph and a cache. When you run cargo build, Cargo checks the fingerprint of every crate in your project. A fingerprint is a hash of the source code, the dependency versions, the compiler version, and the build configuration. If the fingerprint matches what's in the target directory, Cargo skips that crate. If the fingerprint changes, Cargo recompiles it.

This is incremental compilation. It's the reason you can change a function and only recompile your crate, not the entire standard library. Incremental compilation is on by default for dev builds. It trades disk space for time. The target directory grows to store intermediate artifacts. Your build gets faster.

Think of a factory assembly line. Each station produces a component. When the blueprint for one component changes, you only rework that station. You don't tear down the whole factory. But if you change the factory layout, or if you force every station to use a new tool, you have to rework everything. Cargo is the foreman. It watches the blueprints and the tools. If something changes, it orders the rebuild.

// src/main.rs
/// Prints the version of the application.
fn print_version() {
    println!("Version 0.1.0");
}

fn main() {
    print_version();
    // Changing this line updates the fingerprint for main.rs.
    // Cargo recompiles only this crate, not dependencies.
    println!("Hello, world!");
}

Convention: cargo check is the standard tool for fast feedback. It runs type checking and borrowing analysis without generating machine code. It skips the LLVM backend. It's often five to ten times faster than cargo build. Use cargo check while you code. Use cargo build when you need the binary.

The target directory and the nuclear option

The target directory holds everything Cargo builds. It contains debug and release folders for binaries, .fingerprint files for tracking changes, and .incremental folders for cached compiler artifacts. When builds feel stuck, the first instinct is often to delete the directory.

rm -rf target
cargo build

This is the nuclear option. It wipes the cache. It forces a full rebuild of every dependency and your code. It is safe. It is also slow. You should only use it when the cache is corrupted or when you change something that Cargo doesn't track automatically.

Use rm -rf target when you switch Rust toolchain versions. The compiler version is part of the fingerprint, but sometimes metadata lingers. Use rm -rf target when you change the MSRV and dependencies start failing with obscure errors. Use rm -rf target when you suspect stale artifacts are causing link failures.

Don't delete target to fix a typo. Fix the typo. The cache exists to save you time. Wiping it throws that time away.

The global configuration trap

Some projects use configuration flags to tune compilation behavior. One such flag is hint-mostly-unused. This flag tells the compiler to skip certain checks for unused items or to apply specific optimization hints. The flag itself isn't the problem. The problem is how you apply it.

If you set hint-mostly-unused = true for package."*" in your Cargo.toml, you are telling Cargo to apply this flag to every dependency in your graph. This breaks incremental caching. Dependencies are compiled once and cached. When you force a flag on them, Cargo sees a change in the build configuration. It invalidates the cache for every dependency. Every build becomes a full rebuild.

# Cargo.toml
# BAD: Applies the flag to all dependencies.
# Cargo invalidates the cache for every crate in the graph.
# Build times skyrocket.
[package."*"]
hint-mostly-unused = true

# GOOD: Applies the flag only to your crate.
# Dependencies keep their cached artifacts.
# Incremental builds work as expected.
[package]
name = "my-project"
hint-mostly-unused = true

Scope your flags. Global settings are the enemy of incremental builds. If a flag is useful for your code, apply it to your crate. If a dependency needs a flag, check if the dependency supports it via features. Never force configuration on the entire graph unless you have a specific reason and you accept the performance cost.

Profiles and optimization levels

Cargo uses profiles to control compilation settings. The dev profile enables debug info and disables most optimizations. The release profile enables optimizations and strips debug info. Changing the profile changes the fingerprint. Cargo rebuilds everything.

You can tweak profiles in Cargo.toml. Changing opt-level or debug forces a rebuild. This is expected behavior. The compiler generates different code for different optimization levels. The cache must reflect that.

# Cargo.toml
[profile.dev]
# opt-level = 0 is the default. Fast compilation, slower runtime.
# Changing this to 1 or 2 forces a rebuild of all crates.

[profile.release]
# opt-level = 3 is the default. Slow compilation, fast runtime.
# lto = true enables link-time optimization.
# LTO can significantly increase build times.
# Use it only when you need maximum performance.
lto = false

Convention: CI pipelines should disable incremental compilation. Set CARGO_INCREMENTAL=0 in your CI environment. Incremental artifacts are useless in CI because each build starts from a clean state. Disabling incremental saves disk space and avoids cache management overhead. Local builds benefit from incremental; CI builds do not.

Build scripts and hidden costs

Build scripts (build.rs) run before compilation. They can generate code, compile C libraries, or perform other tasks. Build scripts are slow. They run every time the script or its inputs change. If your build script touches the network or compiles external code, it adds latency.

Cargo tracks build script outputs. If the script produces the same output, Cargo skips re-running it. But if the script depends on environment variables or file system state that Cargo doesn't track, it might run unnecessarily.

Keep build scripts minimal. Generate only what you need. Cache external artifacts if possible. If you can avoid a build script, avoid it. Generated code is easier to maintain when it's just Rust code.

Pitfall: Build scripts can cause errors that look like compilation errors. If a build script fails to generate a file, you might see E0463 (can't find crate for library) or E0432 (unresolved import). Check the build script output first. The error is often upstream of the compiler.

When incremental compilation hurts

Incremental compilation isn't free. It uses disk space. It can slow down compilation in some cases. The incremental compiler has to serialize and deserialize metadata. For small projects, the overhead is negligible. For large projects with many crates, the overhead can add up.

If you notice that incremental builds are slower than clean builds, you might be hitting a limitation. The incremental cache can fragment. The metadata can grow too large. In these cases, rebuilding from scratch might be faster.

Use CARGO_INCREMENTAL=0 when you are debugging build performance. Compare the time of a clean build versus an incremental build. If the clean build is faster, the incremental cache is hurting you. You might need to adjust your project structure or reduce the number of crates.

Counter-intuitive but true: the more crates you have, the more likely incremental compilation is to help. But if those crates are tiny and change frequently, the overhead of managing the cache can outweigh the benefits. Measure before you optimize.

Decision matrix for build speed

Use cargo check when you want to verify types and logic without generating binaries. It catches E0308 (mismatched types) and E0382 (use of moved value) in seconds. Save cargo build for when you need to run the program.

Use rm -rf target when you change the Rust toolchain version or encounter stale cache errors. It resets the state. It is the last resort for fixing build issues, not a routine step.

Use scoped feature flags when you need to tune compilation behavior for your crate without invalidating dependency caches. Apply flags to [package], not [package."*"]. Global flags force full rebuilds.

Use CARGO_INCREMENTAL=0 when you are running in a CI environment or when incremental overhead is measured to be higher than the cost of recompilation. Clean builds are predictable. Incremental builds are fast only when the cache is warm.

Use sccache or mold when you have a large project with many dependencies. sccache caches compiler invocations across projects. mold is a faster linker. These tools address bottlenecks that Cargo profiles can't fix.

Where to go next