Skip to content
Select theme

What does cargo check do?

Short answer: cargo check runs the compiler far enough to find every type error, borrow error and lint warning, then stops before code generation. You get the errors without the executable. It is faster than cargo build, but less than you have probably been told.

Compiling a binary happens in two halves. The front half parses, resolves names, does type and borrow checking, and produces metadata. The back half hands that to LLVM, which generates machine code, and then the linker assembles a binary.

cargo check does the first half only. Everything that would make your program compile wrong is still caught; only the work of producing machine code is skipped.

That matters most for dependencies. A library dependency under cargo check is reduced to a .rmeta metadata file. Under cargo build it becomes a compiled .rlib with real machine code in it. So the more dependencies a project has, the wider the gap gets — and the leading cost of a big Rust project is usually compiling its dependencies, not your own crate.

The honest version, since the usual claim is vaguer than the reality. A project of 22,844 lines across 41 modules, no dependencies, on an M-series Mac:

cargo check cargo build
Cold (empty target/) 0.55 s 0.68 s
Warm (one file touched) 0.16 s 0.26 s

Both figures warm are three runs each, and they were stable to the hundredth of a second.

So on a small project with no dependencies, check saves you a fifth of the build. If you have been promised that it is ten times faster, that promise came from someone with a large dependency tree.

The mechanism is worth knowing precisely, because it tells you when the gap will be large for you: count the dependencies. A project with 200 of them spends most of its build time in codegen for code you did not write, and that is exactly the work check removes.

Terminal window
cargo check

It reports errors and warnings the same way cargo build does:

warning: unused variable: `unused`
--> src/main.rs:2:9
|
2 | let unused = 42;
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_unused`
|
= note: `#[warn(unused_variables)]` on by default

That is the loop it belongs in: edit, cargo check, read the errors, edit again. You get the same diagnostics as a build, faster, without leaving an executable behind that is already out of date.

For a loop that responds to file changes automatically:

Terminal window
cargo install cargo-watch
cargo watch -x check

Four things cargo check will not tell you.

Link errors. A missing C library, an undefined symbol, a conflict between two copies of the same crate — all of these happen at link time, which is after check stops. A project can pass cargo check and fail cargo build on a linker not found error. CI must run cargo build.

Runtime behaviour. Check never runs your program. Panics, logic errors and wrong output are invisible to it.

Test code, by default. Plain cargo check covers the default targets only. Your #[cfg(test)] modules and benches/ are not type-checked unless you ask:

Terminal window
cargo check --all-targets
cargo check --all-targets --all-features

This is the command to put in a pre-commit hook. A typo inside a test function that plain cargo check misses will still fail cargo test on CI, and if you use a feature flag (--all-features) plain check only compiles the default feature set.

Anything generated by a build script. Build scripts and proc macros are compiled and executed by check, because the compiler needs their output to continue. So a broken build.rs does fail. What does not happen is any validation of what they produced beyond its shape.

Situation Command
Tight edit loop cargo check
Pre-commit cargo check --all-targets
CI cargo build plus cargo test
Before shipping cargo build --release

check is a fast filter, not a replacement. Nobody ships a check.