Cargo’s incremental compilation is a game-changer for Rust development speed. It’s not just about making builds faster; it’s about making your inner loop — the cycle of coding, building, and testing — feel responsive enough that you never want to leave your editor.

Let’s see it in action. Imagine a simple Rust project.

// src/main.rs
fn main() {
    println!("Hello, world!");
}

First build:

$ cargo build
   Compiling my_project v0.1.0 (/path/to/my_project)
    Finished dev [unoptimized +debuginfo] target(s) in 1.23s

Now, let’s say we make a tiny change, like adding a comment.

// src/main.rs
// This is a comment
fn main() {
    println!("Hello, world!");
}

And build again:

$ cargo build
    Finished dev [unoptimized +debuginfo] target(s) in 0.15s

Notice the difference? The second build was significantly faster because Cargo only recompiled the parts of the code that actually changed. This is incremental compilation.

How it Works: The "Dirty" Files and Artifacts

At its core, incremental compilation relies on tracking which parts of your codebase have changed since the last build. Cargo doesn’t just look at file modification times. It uses a sophisticated system of "artifact dependencies."

When you build, Cargo creates a set of metadata files (in .cargo/incremental/) that describe the state of your compilation artifacts. For each crate, it stores information about its inputs (source files, dependencies, compiler flags) and its outputs (compiled object files, metadata).

When you run cargo build again, Cargo compares the current state of your project against the recorded state in the incremental directory. If a source file hasn’t changed, and its dependencies (other crates, compiler versions, features) are also the same, Cargo can reuse the previously compiled object file. It essentially skips recompiling that unit.

This isn’t just about avoiding recompiling entire crates. Cargo is granular. It can track changes within a crate. If you modify a function but not its signature, only that function’s compiled output might need to be updated. If you change a struct definition, all code that uses that struct will likely need recompilation.

Tuning the Engine: Cargo.toml Levers

The primary way to influence Cargo’s incremental compilation behavior is through your Cargo.toml file, specifically in the [profile.dev] section (for development builds).

  1. incremental: This is the master switch.

    • Default: true (for dev profiles).
    • Purpose: Enables or disables incremental compilation entirely.
    • Why it works: When true, Cargo uses the artifact-based incremental compilation. When false, it performs a full, clean rebuild every time, ignoring all previous compilation states.
    • Example:
      [profile.dev]
      incremental = true # This is the default, usually don't need to set it
      
  2. codegen-units: This is where the real performance gains can be found, but it’s a trade-off.

    • Default: 256 (for dev profiles).
    • Purpose: Controls how many independent compilation units a single crate is broken into. A higher number means more units, which can speed up incremental builds by allowing more fine-grained recompilation, but can slightly slow down release builds due to less optimization across units. A lower number means fewer units, potentially slower incremental builds but faster release builds.
    • Why it works: Each codegen unit is compiled somewhat independently. With many units, changing one small piece of code might only require recompiling its specific unit. With fewer units, a change might invalidate a larger compilation unit. For fast iteration, you want many small units.
    • Example (for faster dev builds):
      [profile.dev]
      codegen-units = 16
      
      Note: Going too low (e.g., 1) can make incremental builds very slow because a single change invalidates the entire crate’s compilation. Setting it too high might not offer much benefit beyond a certain point and can increase memory usage during compilation. Experimentation is key.
  3. debug: Controls the level of debug information.

    • Default: true (for dev profiles).
    • Purpose: Determines whether debug symbols are generated.
    • Why it works: Debug symbols add overhead to the compilation process and the resulting object files. While necessary for debugging, disabling them (though not recommended for dev builds) would speed up compilation. Incremental compilation works with debug symbols enabled.
    • Example:
      [profile.dev]
      debug = true # Default, essential for debugging
      

The Counterintuitive Truth About codegen-units

Many developers assume that codegen-units = 1 is the ultimate goal for any build speed. However, for the development loop, this is often counterproductive. codegen-units = 1 means the entire crate is compiled as a single unit. This maximizes optimization potential for release builds (as the compiler can see all the code at once), but it brutally punishes incremental builds. If even a single line in a large crate changes, the entire crate must be recompiled because it’s all one unit. For rapid iteration, you want to break your crate into as many small, independent compilation units as your system can handle without excessive memory usage. This allows Cargo to only recompile the affected few units, leading to seconds saved per build.

The next hurdle you’ll face is understanding how Cargo handles feature flags and their impact on incremental compilation invalidation.

Want structured learning?

Take the full Cargo course →