What Is cargo check and Why Is It Faster Than cargo build?

`cargo check` compiles your code just enough to verify syntax and type correctness without generating executable binaries, making it significantly faster than `cargo build` because it skips the time-consuming linking and code generation phases.

The spinner is the enemy

You are refactoring a module. You rename a function, update the call sites, and hit save. The terminal spins. Five seconds pass. Ten. You are already losing your train of thought. You spot a typo, fix it, and hit save again. The spinner returns. By the time the build finishes, you have forgotten what you were trying to fix. This loop kills flow state.

Rust's compiler is thorough. It checks types, lifetimes, and memory safety with absolute rigor. That rigor is what makes Rust safe. It also makes compilation feel heavy when you are iterating quickly. You do not need a runnable binary every time you type a character. You need to know if the types match. You need to know if the borrows hold. You do not need the machine code yet.

cargo check solves this. It runs the compiler's analysis phases and stops before code generation. It gives you the safety guarantees without the wait.

Check versus build

cargo build performs the entire compilation pipeline. It parses your code, checks types, runs the borrow checker, generates machine code, and links libraries into a final executable. It produces a binary you can run.

cargo check stops after the borrow checker. It verifies syntax, types, and lifetimes. It catches the same errors as cargo build regarding correctness. It never generates machine code. It never links. It never produces a binary. It just tells you whether your code is valid Rust.

Think of cargo check as a proofreader and cargo build as a typesetter. The proofreader reads your manuscript, flags typos, and highlights grammar issues. It does not touch the printing press. The typesetter formats the pages, binds the cover, and outputs a physical book. You run the proofreader constantly. You run the typesetter only when you need the book.

Skip the heavy lifting until you actually need to run the code.

Minimal example

Here is the smallest case: a function, a type mismatch, and a quick check.

// src/main.rs

/// Calculates the area of a rectangle.
fn area(width: f64, height: f64) -> f64 {
    width * height
}

fn main() {
    // Intentional error: passing a string where a float is expected
    // This will trigger a type mismatch during analysis
    let result = area("10", 5.0);
    println!("Area: {}", result);
}

Run cargo check to verify the code.

$ cargo check
Checking my_app v0.1.0
error[E0308]: mismatched types
 --> src/main.rs:9:27
  |
9 |     let result = area("10", 5.0);
  |                           ^^^^ expected `f64`, found `&str`

cargo check catches E0308 instantly. It reports the mismatched types. It does not generate a binary. It does not link. The feedback is immediate.

Fix the error and run cargo check again.

// src/main.rs

fn area(width: f64, height: f64) -> f64 {
    width * height
}

fn main() {
    // Fixed: passing a float to match the signature
    // The borrow checker and type checker will now pass
    let result = area(10.0, 5.0);
    println!("Area: {}", result);
}
$ cargo check
Checking my_app v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s

The check passes. The code is valid. You can now run cargo build or cargo run when you are ready to execute.

Trust the green checkmark. It means the compiler is happy with your types.

Under the hood

The Rust compiler, rustc, works in distinct phases. Understanding these phases explains why cargo check is faster.

Parsing happens first. The compiler reads source files and converts them into tokens. It builds an Abstract Syntax Tree. Next comes HIR construction. The compiler builds the High-Level Intermediate Representation. This is a richer structure that includes type information and lifetime annotations. Type checking follows. The compiler verifies that types match. It checks function arguments, return values, and trait bounds. Borrow checking comes after. The compiler verifies memory safety. It ensures references are valid and no data races exist. This is where E0382 (use of moved value) and E0502 (borrow conflicts) are caught.

Code generation is the heavy lift. The compiler translates the HIR into LLVM IR and then into assembly or machine code. This phase is CPU-intensive. It optimizes the code and emits object files. Linking finishes the pipeline. The linker combines object files and libraries into a final binary. It resolves symbols and handles dependencies. This phase can be memory-intensive and slow for large projects with many dependencies.

cargo check stops after the borrow checker. It runs parsing, HIR construction, type checking, and borrow checking. It skips code generation and linking. Code generation is often the most expensive step. Linking can add significant overhead, especially when merging thousands of symbols from dependencies. By skipping these steps, cargo check avoids the bulk of the work.

Both cargo check and cargo build use incremental compilation. The compiler stores intermediate artifacts in the target directory. If you run cargo check, it saves analysis artifacts. If you run cargo build later, it reuses those artifacts. The order matters less than you might think, but cargo check remains faster because it does less work even with cached artifacts.

Convention aside: the community treats cargo check as the default save-guard. Most developers alias it to a keyboard shortcut or rely on their editor to run it automatically. You do not need to type it manually every time.

Let the compiler do the cheap work first. Save the expensive work for later.

Realistic workflow

In a real project, you often have multiple targets. A library crate might have several binary targets. You might be working on the library logic and not care about the binaries yet.

Here is a Cargo.toml with a library and two separate binaries.

# Cargo.toml

[package]
name = "my_tool"
# Defines the crate name and version metadata

[[bin]]
name = "cli"
path = "src/bin/cli.rs"
# Registers the first binary target

[[bin]]
name = "daemon"
path = "src/bin/daemon.rs"
# Registers the second binary target

[lib]
name = "my_lib"
path = "src/lib.rs"
# Declares the library crate for internal logic

You are editing src/lib.rs. You want to check the library without compiling the binaries.

# Check only the library crate. Skips cli and daemon binaries.
# The --lib flag tells cargo to ignore all [[bin]] targets
cargo check --lib

This command validates the library code. It ignores cli.rs and daemon.rs. If those files have errors, cargo check --lib does not report them. You focus on the library. This speeds up the feedback loop when you are working on a specific part of the codebase.

You can also check specific binaries.

# Check only the cli binary
# Isolates compilation to a single executable target
cargo check --bin cli

Feature flags are another common scenario. You might have a feature that enables heavy dependencies.

# Check with a specific feature enabled
# Compiles conditional #[cfg(feature = "...")] blocks
cargo check --features "experimental"

This validates the code with the feature active. It catches errors that only appear when the feature is enabled. It is faster than building because it skips code generation.

Target what you touch. Leave the rest alone.

Pitfalls and limits

cargo check is powerful, but it has limits. It does not replace testing. It does not replace running the code.

cargo check catches compile-time errors. It catches E0308 (mismatched types). It catches E0382 (use of moved value). It catches E0502 (borrow conflicts). It catches E0277 (trait bounds not satisfied). It does not catch runtime errors. It does not catch panics. It does not catch unwrap() on None. It does not catch logic bugs. It does not catch segfaults in unsafe blocks, though it checks the syntax of unsafe blocks.

If your code compiles but crashes at runtime, cargo check will not tell you. You need cargo test to verify runtime behavior. You need cargo run to observe execution.

Another pitfall is assuming cargo check is enough for CI. Continuous integration pipelines should run cargo test and cargo build to ensure the binary produces correctly. cargo check is a development tool. It speeds up your local workflow. It does not guarantee the build succeeds in all environments.

Convention aside: rust-analyzer runs cargo check logic in the background. Your editor shows errors almost instantly. You do not need to run cargo check manually if your editor is configured. However, running cargo check in the terminal is useful for pre-commit hooks or when you want a clean slate check.

A green checkmark only means the compiler is happy. It does not mean your program works.

Decision matrix

Use cargo check when you want rapid feedback on syntax, types, and borrows during development. Use cargo check when you are validating a subset of your codebase with --lib or --bin to ignore unrelated targets. Use cargo build when you need the executable binary for distribution, benchmarking, or deployment. Use cargo test when you need to verify runtime behavior and logic correctness. Use cargo run when you want to build and execute in one step, accepting the longer wait time. Use cargo clippy when you want lint suggestions.

Check compiles. Test runs. Do not confuse the two.

Where to go next