How to Use cargo tree to Debug Dependency Issues

Use cargo tree to visualize your Rust project's dependency graph and debug version conflicts or unused crates.

When the dependency graph turns against you

You just added reqwest to your project, and the build explodes. The error says serde version 1.0.150 conflicts with serde version 1.0.180. You didn't touch serde. You don't even know which crate pulled in the old version. You're staring at a wall of text from cargo build, and the dependency graph feels like a black box. You need to see the lines connecting your crate to the chaos.

cargo tree is the X-ray for your project's supply chain. It prints the full dependency graph, showing exactly which crate depends on which, and which versions are actually resolved. It turns the abstract resolution logic into a concrete map you can read.

The supply chain analogy

Imagine you're baking a cake. Your recipe calls for chocolate. The chocolate brand you buy uses cocoa from a specific farm. That farm uses a specific type of fertilizer. If the fertilizer changes, the taste changes. You didn't buy the fertilizer, but it's in your cake.

In Rust, your Cargo.toml is the recipe. The crates you list are the ingredients. The crates those crates depend on are the cocoa and the fertilizer. cargo tree prints out every single link in that chain. It shows you that reqwest depends on serde, which depends on serde_derive, which depends on proc-macro2. It reveals the transitive dependencies you never asked for but are part of your binary.

The tree is the truth. Your Cargo.toml is just a wish list.

Minimal example

Run cargo tree in your project root to see the normal dependency graph. The output is an ASCII tree where your crate is the root.

# Run this in your project root.
# It prints the tree of normal dependencies.
# The output shows package names, versions, and paths.
cargo tree

The output looks like this:

my_app v0.1.0 (/path/to/my_app)
├── serde v1.0.180
│   ├── serde_derive v1.0.180 (proc-macro)
│   │   └── proc-macro2 v1.0.66
│   └── serde_json v1.0.104
└── tokio v1.28.0
    └── mio v0.8.8

Each line is a crate. The indentation shows depth. serde_derive is a dependency of serde. proc-macro2 is a dependency of serde_derive. The (proc-macro) tag indicates a procedural macro crate, which is compiled differently and only available at compile time.

Run cargo tree before you run cargo build when things get weird. See the graph first.

How the resolution works

When you run cargo tree, Cargo reads your Cargo.lock file. This file locks the exact versions of every crate in your project. Cargo builds the graph in memory and prints it as an ASCII tree.

The lock file is the source of truth. If you don't have a Cargo.lock, Cargo resolves the graph on the fly. This can lead to different results than your build if the registry has changed. Always generate the lock file first.

# Generate or update the lock file.
# This ensures cargo tree reads the same versions as cargo build.
cargo generate-lockfile

The tree shows the resolved state. If a crate appears multiple times, it means different versions are in use, or the same version is pulled in by multiple paths. Cargo deduplicates crates by default in the display, but the underlying graph can have multiple instances.

The lock file is the source of truth. If the tree looks wrong, check the lock.

Debugging version conflicts

You suspect hyper is pulling in an old version of http. You need to find the path. The -i flag shows the inverse tree. It lists every crate that depends on the target, recursively.

# Find which crate depends on 'http' version 0.2.
# The -i flag shows the inverse tree: who depends on 'http'?
# This is faster than grep when the tree is huge.
cargo tree -i http

The output shows the paths leading to http. You can see that hyper depends on http, and reqwest depends on hyper. If you see two different versions of http in the tree, you have a conflict. One path pulls in 0.2, another pulls in 1.0.

Convention aside: The community treats cargo tree -i as the standard for debugging. grep works, but -i gives you the dependency path, which is what you actually need to fix the conflict. grep just tells you the crate exists.

Use -i to hunt. Use grep to skim. Know the difference.

Filtering and pruning

Large projects produce massive trees. You can filter the output to focus on what matters. The --prune flag removes subtrees. The --depth flag limits how deep the tree goes.

# Prune the 'serde' subtree to reduce noise.
# This hides all dependencies of serde.
# Useful when you only care about the top level.
cargo tree --prune serde
# Show only the first two levels of dependencies.
# This gives a high-level overview without the leaf crates.
# Good for quick sanity checks.
cargo tree --depth 2

You can also filter by edge type. Dependencies can be normal, dev, or build. By default, cargo tree shows only normal dependencies.

# Show all edges, including dev and build dependencies.
# This reveals crates used only in tests or build scripts.
# Essential for auditing the full project footprint.
cargo tree --edges all

Convention aside: When auditing for security vulnerabilities or license compliance, use --edges all. Dev dependencies can introduce vulnerabilities even if they don't ship in the binary. Build dependencies run on the CI machine and can affect reproducibility.

Don't trust your eyes on the Cargo.toml. Trust the tree. The tree shows what actually compiled.

Customizing the format

The default output includes versions and paths. You can change the format using --format. The format string supports placeholders like {p} for package, {f} for features, {t} for targets, and {d} for dependencies.

# Output just the package names, one per line.
# This is machine-readable and safe to pipe into scripts.
# Convention: use this for automation and CI checks.
cargo tree --format "{p}"
# Show packages and their enabled features.
# Features can add or remove dependencies.
# This helps debug why a dependency appeared unexpectedly.
cargo tree --format "{p} {f}"

Features change the tree. Enabling a feature can add a dependency that wasn't there before. If you see a crate in the tree that you don't recognize, check the features of its parent.

Features change the tree. Always check features when debugging dependency issues.

Handling duplicates

When multiple versions of a crate exist, cargo tree shows them separately. The --duplicates flag highlights crates that appear more than once. This is useful for identifying bloat or conflicts.

# List only crates that have multiple versions in the tree.
# This helps identify version fragmentation.
# High duplicate counts can increase compile times.
cargo tree --duplicates

The output lists the crate name and the count. If you see serde with count 2, it means two versions of serde are in the graph. This can happen if one crate requires 1.0.150 and another requires 1.0.180. Cargo can't unify them, so both are compiled.

Convention aside: The community considers duplicate dependencies a code smell. They increase binary size and compile times. Use --duplicates regularly to keep the graph clean. If you find duplicates, check if you can update the upstream crates or use [patch] in Cargo.toml to force a single version.

Counter-intuitive but true: the more you use unsafe, the harder the rest of your code becomes to reason about. Similarly, the more duplicates you have, the harder the dependency graph becomes to maintain.

Pitfalls and edge cases

Dev dependencies are hidden by default. If you think a crate is unused, it might be used in tests. Run cargo tree --edges dev to see them.

Build dependencies are also hidden. They run during the build process and can affect the output. Run cargo tree --edges build to see them.

Proc-macro dependencies show up as (proc-macro). They are only available at compile time. They don't end up in the final binary, but they affect compile times.

The tree reflects the resolved state. If you have a version conflict, cargo tree won't show the conflict directly. It shows the resolved graph. The conflict shows up in cargo build with "failed to select a version for...". Use cargo tree to understand the graph, then use cargo build to see the error.

Convention aside: Always commit Cargo.lock for binary crates. cargo tree reads the lock file. If you don't commit it, your tree might resolve differently on CI than on your laptop. This leads to "it works on my machine" bugs.

Don't fight the compiler here. Reach for cargo tree to understand the graph, then fix the Cargo.toml to match your intent.

Decision matrix

Use cargo tree when you need a quick overview of your normal dependencies and want to verify the top-level structure. Use cargo tree -i crate_name when you need to find every path that pulls in a specific crate, especially to debug version conflicts or duplicate symbols. Use cargo tree --all when you're auditing the entire project, including dev and build dependencies, to check for security vulnerabilities or license compliance. Use cargo tree --format "{p}" when you need a flat list of crates for scripting or piping into other tools. Use cargo tree --edges normal when you want to hide dev dependencies to see only what ships in your binary. Use cargo tree --duplicates when you want to identify version fragmentation and reduce binary bloat. Use cargo tree --prune crate_name when the tree is too large and you want to focus on a specific subtree. Use cargo tree --depth N when you want a high-level view without the leaf crates.

Pick the flag that matches the question. Don't parse the whole tree if you only need one branch.

Where to go next