The Cargo Guide
Inspecting and Changing Dependencies
Why Dependency Inspection and Change Management Matter
Once a Cargo package has dependencies, the job is no longer just to declare them. You also need to inspect the resolved graph, change dependencies intentionally, understand what changed, and avoid unnecessary churn.
A useful mental model is:
- declaring dependencies defines policy
- resolving dependencies produces a concrete graph
- inspecting dependencies shows what actually happened
- changing dependencies safely means understanding both the manifest and the lockfile
The Main Activities in This Area
The core activities covered in this guide are:
- adding dependencies
- removing dependencies
- updating dependencies
- inspecting the dependency tree
- understanding what changed in the graph
- selectively updating one package
- patching one crate for testing or temporary overrides
- minimizing lockfile churn
- tracing why a crate appears in the graph
These activities are connected. A dependency change is easiest to understand when you can inspect the graph before and after.
A Small Example Package
Suppose we start with a small package:
cargo new dep_manage_demo --lib
cd dep_manage_demoInitial manifest:
[package]
name = "dep_manage_demo"
version = "0.1.0"
edition = "2024"
[dependencies]Simple source:
pub fn greet(name: &str) -> String {
format!("Hello, {name}!")
}This package is small enough to understand clearly, but it can still demonstrate the full dependency management workflow.
Adding a Dependency by Editing Cargo.toml
The traditional way to add a dependency is to edit Cargo.toml directly.
For example:
[dependencies]
regex = "1"Then use it in code:
use regex::Regex;
pub fn contains_number(s: &str) -> bool {
Regex::new(r"\d").unwrap().is_match(s)
}Then build:
cargo buildThis works well and teaches the structure of the manifest directly.
Adding a Dependency with cargo add
Cargo also provides cargo add to add or modify dependencies from the command line.
Add a normal registry dependency:
cargo add regexAdd a specific requirement:
cargo add serde@1Add a dev dependency:
cargo add --dev pretty_assertionsAdd a build dependency:
cargo add --build ccAdd a path dependency:
cargo add --path ../core_utils core_utilsAdd a git dependency:
cargo add --git https://github.com/example/parser.git parserA strong practical model is:
- direct
Cargo.tomlediting teaches manifest structure cargo addis often faster and more uniform in day-to-day work
Adding Optional and Renamed Dependencies
You can also add more specialized dependencies.
Optional dependency:
cargo add serde --optional --features deriveThis may produce something like:
[dependencies]
serde = { version = "1", optional = true, features = ["derive"] }Renamed dependency:
cargo add serde_json --rename json_coreWhich may produce:
[dependencies]
json_core = { version = "1", package = "serde_json" }Then in Rust code:
pub fn parse_value() -> json_core::Value {
json_core::from_str("{\"x\":1}").unwrap()
}This helps keep dependency naming intentional rather than accidental.
Removing Dependencies
You can remove dependencies either by editing Cargo.toml manually or by using cargo remove.
For example:
cargo remove regexRemove a dev dependency:
cargo remove --dev pretty_assertionsRemove a build dependency:
cargo remove --build ccIf you remove a dependency from the manifest, you should also remove or refactor any code that still uses it.
For example, after removing regex, code like this must change:
use regex::Regex;
pub fn contains_number(s: &str) -> bool {
Regex::new(r"\d").unwrap().is_match(s)
}Updating Dependencies
Dependency updates often happen through the lockfile rather than through manifest edits.
Update the whole lockfile to the newest allowed versions:
cargo updateUpdate one package selectively:
cargo update -p regexA useful mental model is:
- edit
Cargo.tomlwhen policy should change - run
cargo updatewhen you want the lockfile to move to newer versions already allowed by policy
When a Manifest Edit Is Needed vs When cargo update Is Enough
Suppose your manifest contains:
[dependencies]
regex = "1"If a newer compatible 1.x version exists, you often do not need to change the manifest at all. You can usually update the lockfile:
cargo update -p regexBut if you want to move to a new major version line or otherwise change policy, then you need to edit the manifest:
[dependencies]
regex = "2"Then build and test again:
cargo build
cargo testThis distinction is one of the most important ideas in dependency maintenance.
Inspecting the Dependency Tree
The main tool for inspecting the resolved graph is cargo tree.
cargo treeThis shows the current dependency tree as Cargo resolved it.
A useful model is:
Cargo.tomlshows what you asked forcargo treeshows what Cargo actually resolved
That makes cargo tree the first command to reach for when the graph feels bigger or stranger than expected.
Finding Duplicate Versions
To inspect duplicate crate versions in the graph, use:
cargo tree -dThis is especially useful when:
- compile times feel unexpectedly large
- binary size seems larger than expected
- you suspect multiple versions of the same crate are being compiled
A duplicate is not automatically a bug, but it is often a useful signal that the graph deserves closer inspection.
Inspecting Feature Flow
To inspect how features are being enabled across the graph, use:
cargo tree -e featuresThis helps explain why a dependency ended up with more features enabled than you expected.
For example, you might think only one feature is active, but the graph may reveal that another dependency path requested additional features too.
Auditing Which Package Pulled in What
A very common question is:
"Why is this crate in my graph at all?"
To investigate that, use an inverted tree:
cargo tree -i some_crateThis shows which package paths lead into some_crate.
That is one of the most effective ways to audit which dependency pulled in a surprising transitive crate.
A Concrete Tree Inspection Example
Suppose your manifest says:
[dependencies]
regex = "1"
serde_json = "1"And your code says:
use regex::Regex;
pub fn has_number(s: &str) -> bool {
Regex::new(r"\d").unwrap().is_match(s)
}You can inspect the resolved graph with:
cargo treeThen inspect duplicates:
cargo tree -dThen ask who pulled in a specific crate:
cargo tree -i serde_coreEven if you never named serde_core directly, this helps you locate the path by which it entered the graph.
Reasoning About Outdated Dependencies
In practice, "outdated" rarely means only "not the newest release." A dependency may be outdated in several senses:
- the lockfile is behind the newest version already allowed by your manifest
- the manifest requirement is pinned to an older policy than you want
- the graph contains older duplicates because transitive dependencies are not aligned
- a dependency is effectively stale because it remains in the graph even though your code no longer needs it
A good dependency review asks not just "is there something newer?" but also:
- do we want to move?
- would the move change policy or only lockfile state?
- what graph churn would it cause?
Understanding What Changed in the Graph
When dependencies change, it is useful to inspect both the manifest and the resolved graph.
A practical workflow is:
git diff Cargo.toml Cargo.lock
cargo treeThis helps answer two separate questions:
- what dependency intent changed?
- what concrete graph changed?
Those are related, but they are not the same question.
A Before-and-After Workflow
Suppose you start with:
[dependencies]
regex = "1"Then add another dependency:
cargo add serde_jsonNow inspect changes:
git diff Cargo.toml Cargo.lock
cargo treeThis lets you see:
- the manifest edit introducing
serde_json - the lockfile changes for
serde_jsonand its transitive graph - the actual resolved tree after the change
This is much more informative than only looking at the manifest.
Selective Updates to Minimize Churn
When you only want to move one package and minimize graph churn, selective updates are often better than a broad cargo update.
Example:
cargo update -p regexThis helps when:
- a single dependency has a desired fix
- you want a narrower change for review
- you do not want to refresh the entire lockfile opportunistically
This is one of the easiest ways to keep dependency movement intentional.
Patching One Crate with [patch]
Cargo supports overriding a dependency source with [patch], which is especially useful for testing a fix or working with an unpublished dependency change.
Example using a local path override:
[dependencies]
uuid = "1"
[patch.crates-io]
uuid = { path = "../uuid" }This tells Cargo:
- normal manifest policy still says
uuidcomes from crates.io - but during resolution, use the local path version instead
This is very useful when testing a bugfix before it is published.
A Practical [patch] Example
Suppose you depend on a crate named text_core from crates.io:
[dependencies]
text_core = "1"You discover a bug and fix it in a local checkout. You can patch it in:
[patch.crates-io]
text_core = { path = "../text_core" }Then rebuild:
cargo build
cargo testThis lets you test your local fixed version without rewriting the ordinary dependency declaration.
When to Use [patch] vs When to Edit the Dependency Directly
A good mental rule is:
- use ordinary dependency edits when the source policy itself should change
- use
[patch]when you want a temporary or local override layered on top of normal dependency intent
For example, this changes policy directly:
[dependencies]
text_core = { path = "../text_core" }Whereas this keeps the normal registry dependency but overrides resolution locally:
[dependencies]
text_core = "1"
[patch.crates-io]
text_core = { path = "../text_core" }That distinction matters a lot in collaborative workflows.
Minimizing Churn in Team Workflows
Dependency changes are easier to review when they are narrow and intentional.
A simple low-churn workflow looks like this:
cargo add serde_json
cargo build
cargo test
git diff Cargo.toml Cargo.lockOr for an update:
cargo update -p regex
cargo test
git diff Cargo.lockThis reduces the chance that one small change quietly drags a large unrelated graph refresh along with it.
Using cargo metadata for Machine-Readable Inspection
For tooling and deeper automation, Cargo provides machine-readable metadata output.
cargo metadata --format-version 1This is useful when:
- you want to script dependency inspection
- editor or CI tooling needs structured graph data
- you want more than a terminal tree view
A practical mental model is:
cargo treeis human-facing graph inspectioncargo metadatais tool-facing graph inspection
Understanding Why the Graph Changed
When the graph changes unexpectedly, the main causes are usually one of:
- you edited
Cargo.toml - a lockfile update moved a package within an allowed range
- a new dependency pulled in a larger transitive graph
- feature activation changed the effective graph
- a patch override changed the source or shape of the graph
That is why the best diagnosis flow usually combines:
git diff Cargo.toml Cargo.lock
cargo tree
cargo tree -d
cargo tree -e featuresThis lets you compare declared changes, lockfile changes, duplicate versions, and feature flow together.
Removing Dead Dependencies
Sometimes dependency maintenance is not about adding or updating. It is about removing a dependency that is no longer needed.
Suppose the manifest still contains:
[dependencies]
regex = "1"But your code no longer uses it. Then you can remove it:
cargo remove regex
cargo build
cargo testThis helps keep compile times, graph size, and maintenance burden lower.
A Full Dependency Maintenance Session
Here is a realistic maintenance sequence for one package.
Add a dependency:
cargo add serde_jsonInspect the graph:
cargo treeUpdate one crate selectively:
cargo update -p regexInspect duplicates:
cargo tree -dTrace who pulled in a surprising crate:
cargo tree -i some_cratePatch a local fix:
[patch.crates-io]
some_crate = { path = "../some_crate" }Then verify:
cargo build
cargo testThis is the kind of session where Cargo stops feeling like a simple package manager and starts feeling like a graph management tool.
Common Beginner Mistakes
Mistake 1: assuming Cargo.toml alone tells the whole dependency story.
Mistake 2: using broad cargo update when a selective update would be clearer.
Mistake 3: removing a dependency from the manifest without removing code that still uses it.
Mistake 4: trying to diagnose graph surprises without using cargo tree.
Mistake 5: rewriting dependency sources directly when [patch] would be the cleaner temporary override.
Mistake 6: treating lockfile changes as meaningless noise instead of part of the dependency review.
Hands-On Exercise
Create a small package and practice the full inspection-and-change loop.
Start here:
cargo new dep_change_lab --lib
cd dep_change_labAdd dependencies:
cargo add regex
cargo add serde_json
cargo add --dev pretty_assertionsWrite some code:
use regex::Regex;
pub fn has_number(s: &str) -> bool {
Regex::new(r"\d").unwrap().is_match(s)
}Now inspect the graph:
cargo tree
cargo tree -d
cargo tree -e featuresThen remove one dependency:
cargo remove serde_jsonThen update another selectively:
cargo update -p regexFinally inspect what changed:
git diff Cargo.toml Cargo.lock
cargo treeThis exercise makes the relationship between manifest edits, lockfile changes, and graph inspection concrete.
Mental Model Summary
A strong mental model for inspecting and changing dependencies in Cargo is:
- adding and removing dependencies changes manifest policy
- updating dependencies often changes the lockfile's concrete graph rather than the manifest policy itself
cargo treeshows what Cargo actually resolvedcargo tree -dhelps find duplicate versionscargo tree -i some_cratehelps explain why a crate is present[patch]is the right tool for temporary or local dependency overrides- selective updates and narrow edits reduce churn
- reviewing both
Cargo.tomlandCargo.lockis the best way to understand what changed
Once this model is stable, dependency maintenance becomes much easier to treat as controlled graph management rather than guesswork.
