Cargo
Cargo is Rust's official build system and package manager, and one of the most praised parts of the language. A single tool creates projects, resolves and downloads dependencies from crates.io, compiles code, runs tests and benchmarks, generates documentation, lints and formats code, and publishes libraries. There's no Makefile, CMake, or separate package manager to wire up.
Knowing Cargo well means faster builds, smaller binaries, cleaner multi-crate repositories, and well-behaved libraries. This page covers the manifest, dependency resolution, features, workspaces, profiles, and the everyday subcommands.
TL;DR
cargo new app(binary) orcargo new --lib mylib(library) scaffolds a project withCargo.tomlandsrc/.Cargo.tomldeclares the package and dependencies;Cargo.lockpins exact versions.- Dependency versions use semver-compatible ranges by default:
serde = "1.0"means>=1.0.0, <2.0.0. - Features are opt-in compile-time flags that enable optional code and dependencies.
- Workspaces manage many crates in one repo with a shared lockfile and target directory.
- Daily commands:
cargo build,run,test,check,clippy,fmt,doc, andcargo build --releasefor optimized builds.
Quick Example
Core Concepts
Packages, Crates, and Targets
A package is defined by one Cargo.toml. It contains one or more crates (compilation units): at most one library (src/lib.rs) and any number of binaries (src/main.rs, src/bin/*.rs), plus examples (examples/), integration tests (tests/), and benchmarks (benches/). Cargo discovers these by convention, with no configuration needed.
Dependencies and Versions
Cargo assumes semver: within 1.x, updates must be compatible. Note that for 0.x versions, the minor number is treated as breaking ("0.8" means >=0.8.0, <0.9.0). Cargo's resolver may compile two incompatible major versions of the same crate side by side when different dependencies require them.
Cargo.lock
The lockfile records the exact version and checksum of every dependency in the graph. cargo update refreshes it within your declared ranges. Commit it for applications, and nowadays for libraries too (Cargo's current guidance), so CI builds are reproducible. Downstream users of a library still resolve fresh versions; the lockfile only affects your own builds.
Features
Features are named, additive flags that conditionally compile code:
Users choose features per dependency (tokio = { version = "1", features = ["rt-multi-thread", "macros"] }) and can opt out of defaults with default-features = false. Features must be additive: enabling one should never break code that works without it, because Cargo unifies features across the whole dependency graph.
Workspaces
All members share one Cargo.lock and one target/ directory, so shared dependencies compile once. workspace.dependencies keeps versions consistent. Run commands for one member with -p api. See monorepos.
Profiles
Tune release for size or speed: lto (link-time optimization), codegen-units = 1, panic = "abort", strip = true, opt-level = "s"/"z" for size. A handy dev tweak is [profile.dev.package."*"] opt-level = 2, which optimizes dependencies while keeping your own code fast to rebuild.
Build Scripts
A build.rs file runs before compiling the package. Use it to compile C code (via the cc crate), generate bindings (bindgen), or generate code from schemas (for example protobufs for gRPC). Build scripts should declare cargo::rerun-if-changed=... so they don't rerun on every build.
The Toolchain Around Cargo
cargo clippy: hundreds of lints for correctness, performance, and idioms.cargo fmt: rustfmt formatting; enforce in CI withcargo fmt --check.cargo doc --open: builds HTML docs from///comments, including dependencies.cargo test: runs unit tests (#[cfg(test)]modules), integration tests intests/, and code examples in doc comments.- Extensions:
cargo-nextest(faster test runner),cargo-auditandcargo-deny(vulnerability and license checks),cargo-watch,cargo-expand,cargo-udeps. - rustup manages toolchains; a
rust-toolchain.tomlpins the version per project.
Best Practices
Use cargo check in the Inner Loop
It skips code generation and is several times faster than cargo build. Editors running rust-analyzer do this continuously.
Minimize Default Features
Library authors should keep default features lean, and application authors should disable defaults on heavy dependencies and enable only what's needed. Fewer features mean faster compiles and smaller binaries.
Audit Dependencies
Run cargo audit or cargo deny check in CI to catch crates with known vulnerabilities, banned licenses, or duplicate versions. Every dependency is code you ship.
Speed Up CI
Cache ~/.cargo/registry, ~/.cargo/git, and target/ (the Swatinem/rust-cache action does this well), use cargo nextest, and consider a faster linker such as mold or lld. Compilation time is Rust's main productivity cost; see build tools.
Common Mistakes
Non-Additive Features
Design features so any combination compiles, or pick one backend at runtime.
Benchmarking Debug Builds
Debug builds can be 10–100× slower than release builds. Always measure performance with --release (or a custom profile with debug symbols plus optimizations for profiling).
Wildcard or Over-Tight Versions
version = "*" accepts anything, including future breaking releases. version = "=1.2.3" in a library blocks users from receiving patches. Use the default caret requirements.
FAQ
Should I commit Cargo.lock for a library?
Cargo's current recommendation is yes for all packages. It makes your own CI and development builds reproducible. Consumers of your library ignore it and resolve dependencies themselves, so it doesn't restrict them. Periodically run CI against the latest dependencies too, to catch breakage early.
Why are Rust builds slow, and how do I speed them up?
Monomorphization, heavy optimization, and large dependency graphs make full builds slow. Incremental debug builds are usually quick. Helpful levers: cargo check, splitting large crates in a workspace, trimming features, a faster linker, sccache for shared caching, and optimizing only dependencies in dev.
What's the difference between cargo install and cargo add?
cargo add adds a library dependency to your project's Cargo.toml. cargo install builds and installs a binary crate (a CLI tool such as ripgrep or cargo-nextest) into ~/.cargo/bin. cargo binstall fetches prebuilt binaries instead of compiling.
How do I publish a crate?
Fill in description, license, and repository in Cargo.toml, run cargo publish --dry-run, then cargo publish with a crates.io API token, or use trusted publishing from CI. Published versions are permanent; you can yank a broken version to stop new projects from depending on it, but not delete it.
Related Topics
- Rust — The language overview
- Package Managers — Cargo compared with npm, pip, and Go modules
- Go Modules — Go's approach to versioning and dependencies
- Monorepos — Workspaces and multi-package repos
- Build Tools — Compilation pipelines and caching