Condensed reference for Cargo's resolver.
Cargo builds a dependency graph using a backtracking algorithm with three core operations:
- Walk dependencies: processes deps in order; ordering influences conflict resolution.
- Unify versions: reuses a single version across the graph whenever requirements overlap, reducing build time and keeping types compatible.
- Pick versions: prefers the highest version satisfying all constraints; backtracks on conflict.
Cargo.lock has the highest priority: once a version is locked, it stays locked until the requirement in Cargo.toml no longer allows it (or cargo update is run).
| Symbol | Example | Meaning | Equivalent Range |
|---|---|---|---|
^ |
^1.2.3 |
Compatible (default if no symbol) | >=1.2.3, <2.0.0 |
~ |
~1.2.3 |
Tilde: patch-level only | >=1.2.3, <1.3.0 |
* |
1.* |
Wildcard | >=1.0.0, <2.0.0 |
= |
=1.2.3 |
Exact match (pins) | 1.2.3 only |
> |
>1.2.3 |
Greater than | >1.2.3 |
>= |
>=1.2.3 |
Greater than (or equal | >=1.2.3 |
< |
<1.2.3 |
Less than | <1.2.3 |
<= |
<=1.2.3 |
Less than or equal | <=1.2.3 |
, |
>=1.2, <1.5 |
Multiple requirements (AND) | intersection of both |
- Bare versions are caret requirements.
"1.2.3"and"^1.2.3"are identical. - Allows updates that do not modify the left-most non-zero component:
1.2.3 := >=1.2.3, <2.0.0 1.2 := >=1.2.0, <2.0.0 1 := >=1.0.0, <2.0.0 0.2.3 := >=0.2.3, <0.3.0 0.2 := >=0.2.0, <0.3.0 0.0.3 := >=0.0.3, <0.0.4 0.0 := >=0.0.0, <0.1.0 0 := >=0.0.0, <1.0.0 - This is the recommended form for almost all dependencies: it gives the resolver maximum flexibility while honoring SemVer.
~1.2.3 := >=1.2.3, <1.3.0
~1.2 := >=1.2.0, <1.3.0
~1 := >=1.0.0, <2.0.0
- Not allowed on crates.io for published dependencies.
- A bare
"*"matches any version and is almost always a mistake. - Useful only in local/path/workspace contexts where you intentionally want "whatever is available."
- Pins to a single version. Use for tightly coupled crate pairs, e.g. a library and its companion proc-macro crate that must share an internal ABI.
- Overuse causes unsolvable conflicts when transitive deps disagree.
>=X.0.0is generally too loose: it allows incompatible major bumps.<Xupper bounds should be avoided unless the crate is known to break above that version; the SemVer-compatible upper bound from^is usually what you want.
">=1.2, <1.5"is an intersection. Useful for narrow MSRV-style windows but, like manual upper bounds, can fragment the graph.
Same syntax, different strategy depending on what you ship.
- Use caret requirements with the true minimum you support (
serde = "1.0.130"). - Avoid committing
Cargo.lock: it is ignored when your crate is a dependency. - Avoid
=and tight upper bounds; they fragment downstream graphs. - Bumping a public dep's major is a breaking change for your crate.
- Commit
Cargo.lock: source of truth for reproducible builds, CI, andcargo install --locked. - Keep caret requirements in
Cargo.toml; the lockfile (not tight ranges) provides reproducibility. - Run
cargo updatedeliberately so lockfile churn shows up as an intentional diff.
Mixed workspaces: commit the lockfile, define shared deps in [workspace.dependencies], and reference them via { workspace = true } to prevent version drift between members.
Cargo unifies compatible requirements into one resolved version:
# Both resolve to a single version in [1.1.0, 2.0.0)
crate-a = { version = "1.0" } # in package A
crate-b = { version = "1.1" } # in package BIncompatible ranges (=0.4.8 vs =0.4.11) produce an error.
SemVer-incompatible ranges ("0.6" vs "0.7") silently produce two copies in the graph. Types from different copies are distinct to the compiler:
downcast_refand trait-object identity checks fail at runtime.- Find duplicates with
cargo tree --duplicates. - Library authors can use the semver trick to bridge versions.
- re-exporting
Features are unified globally: every enabled feature for a crate is enabled everywhere it appears.
Features are not unified across:
- Target-specific dependencies for inactive targets.
[build-dependencies]and[dev-dependencies]vs normal[dependencies].
[package]
resolver = "2"
[dependencies]
log = "0.4"
[build-dependencies]
log = { version = "0.4", features = ["std"] } # "std" stays in build script onlyAdds MSRV-aware resolution: when rust-version is set, the resolver prefers dependency versions compatible with the declared MSRV. Enable explicitly via resolver = "3" or resolver.incompatible-rust-versions = "fallback" in .cargo/config.toml.
linksfield: only one version of a crate that links a given native library may appear in the graph. Different majors oflibgit2-syscannot coexist.- Yanked versions: ignored unless already in
Cargo.lockor pinned viacargo update --precise X.Y.Z. - dev-dependency cycles: permitted; normal dependency cycles are not.
# Why is this dep in the graph?
cargo tree --workspace --target all --all-features --invert <crate>
# Who enabled this feature?
cargo tree --workspace --target all --all-features --edges features --invert <crate>
# Show duplicated versions
cargo tree --duplicates
# Trace the resolver
CARGO_LOG=cargo::core::resolver=trace cargo update- Use the bare/caret form (
"1.2.3") by default. - Specify all three components to declare a true minimum (
"1.2.3", not"1"). - Avoid
>=X.0.0(too loose) and~X.Y(too strict) unless you have a concrete reason. - Bump declared minimums to match what your code actually uses, so downstream resolvers don't pick a broken older version.
- Reserve
=for crate pairs that must share an internal ABI.