How to Resolve Dependency Conflicts in Cargo

Fix Cargo dependency conflicts by adding resolver = "2" or "3" to your workspace Cargo.toml configuration.

When Cargo screams about a crate you never touched

You add a new dependency to your project. You run cargo build. The terminal fills with red text.

error: failed to select a version for the requirement `hyper = ^0.14`
candidate versions found which didn't match: 1.1.0, 1.0.1, 0.14.27
location searched: crates.io index
required by package `reqwest v0.11.22`

You stare at the error. You didn't ask for hyper. You asked for reqwest. You check your Cargo.toml. It looks clean. Cargo is blaming you for a dependency you never mentioned. The build fails, and you feel like the tool is fighting you.

Cargo isn't fighting you. Cargo is doing exactly what you asked, but the constraints in your dependency graph have collided. This is a dependency conflict. It happens when two crates in your project demand incompatible versions of a third crate, or when feature flags clash in ways that break the build.

The solution isn't always to edit versions. Sometimes the fix is telling Cargo to use a smarter algorithm for resolving those constraints. That's what the resolver setting does.

How dependency resolution actually works

Cargo builds a dependency graph. Every crate lists its requirements. Cargo's job is to find a set of versions for every crate that satisfies every requirement simultaneously. This is a constraint satisfaction problem.

Most conflicts fall into two categories.

Version conflicts happen when Crate A requires serde >= 1.0 and Crate B requires serde >= 2.0. If serde 2.0 exists and breaks serde 1.0 compatibility, Cargo cannot satisfy both. It has to pick one, and one crate will fail.

Feature conflicts are subtler. They happen when Crate A requires log with the std feature, and Crate B requires log with a feature that disables std. In older resolution modes, Cargo unifies features. It merges all feature requests for a crate into a single set. If those features are mutually exclusive, the build breaks.

Think of it like ordering food at a shared table. In the old system, everyone's requests get merged into one giant order. If Alice orders "no onions" and Bob orders "extra onions", the kitchen has a problem. There is no single order that satisfies both. In the new system, the kitchen tracks requests per person. Alice gets her plate without onions. Bob gets his plate with onions. The conflict disappears because the constraints are isolated.

Cargo's resolver setting controls how features and dependencies are tracked. The default resolver changed over time. Rust 2018 used resolver 1. Rust 2021 switched the default to resolver 2 for workspaces. Rust 2024 introduces resolver 3 with support for weak dependencies.

If you are seeing feature-related conflicts, you likely need resolver 2 or higher. If you are on Rust 2021 or later and using a workspace, you probably already have resolver 2. In that case, the conflict is likely a version issue, not a resolver issue.

Minimal example: fixing a feature conflict

Resolver 1 unifies features across the entire dependency graph. This causes problems when crates enable features that conflict with each other.

Imagine a workspace with two crates. app uses serde with the derive feature. lib uses serde with the std feature. In resolver 1, Cargo merges these. The final serde compilation gets both derive and std. This usually works.

Now imagine lib uses a crate that enables a feature in serde which disables std. Resolver 1 merges std and no_std. The result is invalid. The build fails with a feature conflict error.

Resolver 2 tracks features per dependency edge. It allows Cargo to resolve features without forcing a global union that breaks compatibility.

Here is how you enable resolver 2.

[workspace]
members = ["app", "lib"]
resolver = "2"

The resolver = "2 line tells Cargo to use the modern resolution algorithm. This is the standard for Rust 2021 and later. If your project is a single crate without a workspace, you can set the resolver in the [package] section.

[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
resolver = "2"

Setting the resolver doesn't change which versions Cargo picks. It changes how Cargo handles features and weak dependencies. If your conflict is purely about versions, changing the resolver won't help. You need to update your dependencies or use a patch.

Run cargo tree --features to see which features are enabled for each crate. This helps you spot feature conflicts before they break the build.

Don't guess which features are clashing. Run cargo tree and read the output.

Walkthrough: what happens when you build

When you run cargo build, Cargo performs several steps.

First, Cargo reads your Cargo.toml and the Cargo.toml files of all your dependencies. It builds a graph of requirements. Each node is a crate. Each edge is a dependency with version constraints and feature requests.

Next, Cargo runs the resolver. The resolver walks the graph and assigns versions to each crate. It tries to find a version that satisfies all constraints. If a crate has multiple requirements, the resolver checks if a single version can satisfy all of them.

In resolver 1, the resolver also merges features. If A depends on X with feature f1, and B depends on X with feature f2, the resolver assigns X with features f1 and f2. If f1 and f2 are incompatible, the resolver fails.

In resolver 2, the resolver tracks features per edge. It allows X to be compiled with a feature set that satisfies A and a different feature set that satisfies B, as long as the crate's manifest supports it. This prevents false conflicts caused by feature unification.

Resolver 3 adds support for weak dependencies. A weak dependency is a dependency that is only required if another feature is enabled. Resolver 3 handles these dependencies more efficiently and allows the dep: syntax for explicit dependency references.

If the resolver finds a valid assignment, Cargo downloads the crates and compiles them. If the resolver fails, Cargo prints an error showing the conflicting requirements.

The error message usually points to the crates causing the conflict. Look for lines like required by package A and required by package B. These tell you which crates are pulling in incompatible versions.

Trust the error message. It tells you exactly which constraints are fighting.

Realistic example: workspace with conflicting tokio features

Workspaces are common places for conflicts. Multiple crates might depend on tokio, but with different feature sets.

Suppose server needs tokio with full features. client needs tokio with rt and sync, but explicitly disables macros to reduce compile time. In resolver 1, Cargo merges these requests. The final tokio gets full features, which includes macros. The client crate compiles with macros enabled, even though it didn't ask for them. This increases compile time and binary size.

In resolver 2, Cargo tracks the features separately. server gets tokio with full. client gets tokio with rt and sync. The features don't leak across crates. This keeps the build lean and prevents feature bloat.

Here is a workspace configuration that uses resolver 2.

[workspace]
members = ["server", "client"]
resolver = "2"

[workspace.dependencies]
tokio = { version = "1.35", features = ["rt", "sync"] }

The [workspace.dependencies] section defines shared dependencies. Each member crate can request additional features without affecting other members.

# server/Cargo.toml
[dependencies]
tokio = { workspace = true, features = ["full"] }
# client/Cargo.toml
[dependencies]
tokio = { workspace = true }

The workspace = true syntax inherits the version from the workspace. The features key adds features specific to that crate. Resolver 2 ensures these features don't merge incorrectly.

If you see tokio features leaking into crates that don't need them, check your resolver version. Switching to resolver 2 often fixes this.

Convention aside: use workspace = true for shared dependencies. It keeps versions consistent and makes updates easier. Don't repeat versions in every crate.

Pitfalls and compiler errors

Dependency conflicts can manifest in different ways. Knowing the symptoms helps you diagnose the cause.

Version conflicts produce errors like failed to select a version for the requirement. This means Cargo cannot find a version that satisfies all constraints. The fix is usually to update one of the dependencies or use a patch.

Feature conflicts might produce compiler errors instead of resolver errors. You might see E0428 (duplicate definitions) or E0119 (conflicting implementations). These errors happen when feature unification causes the same code to be compiled twice with different configurations. Switching to resolver 2 often resolves these errors.

Weak dependency conflicts appear in resolver 3. If you use weak dependencies without resolver 3, you might see errors about missing dependencies. Resolver 3 handles weak dependencies correctly.

Another pitfall is cargo update. Running cargo update updates all dependencies to the latest compatible versions. This can introduce new conflicts if a dependency releases a breaking change. Use cargo update -p package-name to update a single package. This gives you surgical control.

If cargo update breaks your build, revert the Cargo.lock file and update packages one at a time. This helps you identify which update caused the conflict.

Convention aside: commit your Cargo.lock file. It pins exact versions and ensures reproducible builds. Don't ignore lock file changes. They tell you when dependencies are shifting.

Use cargo tree --duplicates to find crates that appear multiple times with different versions. This helps you spot version conflicts that might cause runtime issues.

Run cargo tree --invert package-name to see which crates depend on a specific package. This helps you trace conflicts back to their source.

Decision: when to use which resolver and tool

Use resolver = "2" when you have a workspace and feature conflicts. Resolver 2 is the standard for Rust 2021 and later. It prevents feature unification bugs and keeps feature sets isolated per crate. If you are on Rust 2021 or later, you likely already have this. If you are on Rust 2018, upgrade your edition or add resolver = "2" to your workspace.

Use resolver = "3" when you are on Rust 2024 and need weak dependencies. Resolver 3 introduces weak dependency support and the dep: syntax. It allows you to declare dependencies that are only required when specific features are enabled. This reduces compile times and binary sizes for large projects. If you are not using weak dependencies, resolver 2 is sufficient.

Use cargo update -p when you need to update a single transitive dependency. This avoids updating the entire dependency tree and reduces the risk of introducing new conflicts. Pin the version with --precise if you need to test a specific release.

Use [patch] when you need to override a dependency with a local fork. Patches let you replace a crate from crates.io with a local path or git repository. This is useful for debugging upstream issues or testing fixes before they are released. Patches apply to the entire workspace.

Reach for cargo tree when you are confused about why a crate is included. The tree command shows the full dependency graph. Use --features to see feature flags. Use --invert to find reverse dependencies. Use --duplicates to find version mismatches.

Use workspace dependencies when multiple crates share a dependency. Define the dependency in [workspace.dependencies] and reference it with workspace = true. This keeps versions consistent and simplifies updates.

Don't fight the compiler here. Read the graph, pick the right resolver, and let Cargo do the heavy lifting.

Where to go next