Someone adds a one-line dependency and your binary gains forty crates. Or a security advisory names a crate you have never heard of, and you need to know who brought it in. cargo tree answers both, and it has four flags that cover nearly every question: invert, duplicates, features and depth. This post runs each on a real dependency graph, shows how to read the output, and ends with the cases where the tree misleads.
The graph is a small program depending on serde_json 1 and regex 1, eleven crates in total, built with Cargo 1.99 and edition 2024. Paths are shortened.
The whole graph, then one level
cargo tree on its own prints everything, indented by who depends on whom, which is too much for a first look. Ask for one level:
cargo tree --depth 1pf v0.1.0 (/work/pf)
├── regex v1.13.1
└── serde_json v1.0.154Those are the two crates named in Cargo.toml. Everything else is a transitive dependency, and the next question is usually about one of those.
Why is this crate here? Invert the tree
-i (--invert) takes a package and prints the paths to it, from the crate outward to your own:
cargo tree -i memchrmemchr v2.8.3
├── aho-corasick v1.1.5
│ ├── regex v1.13.1
│ │ └── pf v0.1.0 (/work/pf)
│ └── regex-automata v0.4.18
│ └── regex v1.13.1 (*)
├── regex v1.13.1 (*)
├── regex-automata v0.4.18 (*)
└── serde_json v1.0.154
└── pf v0.1.0 (/work/pf)Read it from the top: memchr is wanted by aho-corasick, regex, regex-automata and serde_json, and each of those traces up to pf. An asterisk marks a subtree already printed above, so the listing stays finite. To remove memchr you would have to remove every branch, which here means dropping both direct dependencies, so the answer to "can I get rid of it" is no. That is the typical result, and knowing it in ten seconds beats an hour of editing manifests. When the inverted tree has a single branch, the crate that owns that branch is the one to question.
Which features pulled it in
A crate often arrives because a feature enabled an optional dependency. -e features adds feature edges, and combined with -i it shows who turned each one on:
cargo tree -e features -i regex-automataregex-automata v0.4.18
├── regex-automata feature "alloc"
│ └── regex v1.13.1
│ ├── regex feature "default"
│ │ └── pf v0.1.0 (/work/pf)
│ │ └── pf feature "default" (command-line)
│ ├── regex feature "perf"
│ │ └── regex feature "default" (*)Now the answer is at feature level: the program asked for the default features of regex, which include perf, which turns on more of regex-automata. If you do not need that, regex = { version = "1", default-features = false, features = ["std"] } prunes it; see optional dependencies for the mechanics. In the earlier build-speed measurement that trim saved 7 percent of a clean build on this tree, and the same trick saves far more where the feature drags in a network stack.
Duplicates
cargo tree -d lists crates that appear at more than one version, the usual cause of a slow build, a large binary and a type mismatch between "two" copies of one library. On this graph it has nothing to say:
$ cargo tree -d
warning: nothing to print.
To find dependencies that require specific target platforms, try to use option `--target all` first, and then narrow your search scope accordingly.Treat that message as good news. When it does print something, each version appears with its own inverted tree, so you can see which dependent holds back the old copy. The fix is nearly always upstream: update the dependent that pins the older major, or allow both, because Cargo will build two incompatible majors side by side. See multiple versions in the graph.
Shaping the output for a script or a review
A few formatting flags make the output usable in a pull request or a pipeline:
cargo tree --prefix none --depth 1 # a flat list, no drawing characters
cargo tree --charset ascii # for terminals and logs that mangle the box characters
cargo tree -e no-dev # leave out dev-dependencies
cargo tree --edges build,normal # only build scripts and runtime
cargo tree --target all # include dependencies for other platforms
cargo tree --format "{p} {l}" # name, version and license$ cargo tree --prefix none --depth 1
pf v0.1.0 (/work/pf)
regex v1.13.1
serde_json v1.0.154The license format is worth keeping: cargo tree --prefix none --format "{p} {l}" | sort -u is a one-line license inventory, and cargo deny turns it into a policy.
A failing question: asking about the wrong thing
Two mistakes appear constantly. Asking for a crate that is not in the graph gives an error naming the spec:
$ cargo tree -i tokio
error: package ID specification `tokio` did not match any packagesThat is a useful answer, not a failure: the crate is not there. The other mistake is trusting the output for something it does not model. cargo tree shows the resolved graph for the current target and feature set. A dependency under [target.'cfg(windows)'.dependencies] disappears on a Mac unless you pass --target all, which is also why a "nothing to print" duplicates report on one machine can differ from the same command on a Windows CI runner. Build scripts that download or vendor code at build time are invisible entirely.
Worked example: deciding whether to drop a dependency
Suppose a review asks whether serde_json is worth keeping, because the program only parses one tiny document. Run the three commands in order. cargo tree --depth 1 confirms it is a direct dependency. cargo tree -i serde_json prints one line plus pf, so nothing else needs it, and removing it removes only its own subtree. Then count the cost of that subtree:
cargo tree -p serde_json --prefix none | sort -uOn this graph that lists serde_json plus its four small dependencies, itoa, memchr, serde_core and zmij. But memchr is also used by regex, as the inverted tree showed, so removing serde_json sheds three crates, not four. The decision is then an informed one: three small crates against the code needed to parse the document by hand. For a document that never changes shape, a hand parser may win. For anything user-supplied, keep the library.
Notice what the example needed: not the full tree, but two targeted questions and a set difference. That is the habit worth building. A full cargo tree on a real application runs to thousands of lines, and nobody reads those; the flags exist so you never have to.
A short workflow for a dependency review
cargo tree --depth 1before and after adding a crate; the difference is the direct cost.cargo tree --prefix none | sort -u | wc -lcounts unique crates (11 here); track the number over time.cargo tree -dafter every update.cargo tree -i <crate>for any name in an advisory, then fix the branch that owns it.
When the tree is not the right tool
It tells you what is in the graph, not whether any of it is safe, maintained or necessary; that is the job of audit and deny and vet. For compile cost, cargo build --timings shows time per crate, which is a different question from "how many". The flag reference is in the cargo tree manual.



