Rust's resolver treats rand_core 0.6.4 and rand_core 0.9.5 as two unrelated crates, since a version outside the caret range is incompatible by definition. Most of the time that is a feature: your project and a dependency can each use the version they like. The cost appears the day a value made by one copy is passed to a function expecting the other, and the compiler reports an error that looks absurd on first reading. "The trait RngCore is not implemented for ThreadRng", it says, and ThreadRng plainly implements RngCore. This post reproduces that error with real output, shows how to confirm duplicates are the cause, and lays out how to fix them in the order of least effort. Cargo 1.99.
The same name, two crates
A small project asks for rand 0.8, which depends on rand_core 0.6, and also names rand_core 0.9 directly to write a generic function:
[package]
name = "dv"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.8"
rand_core = "0.9"use rand_core::RngCore;
fn roll(rng: &mut impl RngCore) -> u32 {
rng.next_u32() % 6 + 1
}
fn main() {
let mut rng = rand::thread_rng();
println!("{}", roll(&mut rng));
}cargo builderror[E0277]: the trait bound `ThreadRng: rand_core::RngCore` is not satisfied
--> src/main.rs:9:25
|
9 | println!("{}", roll(&mut rng));
| ---- ^^^^^^^^ the trait `DerefMut` is not implemented for `ThreadRng`
| |
| required by a bound introduced by this call
|
help: the following other types implement trait `rand_core::RngCore`
--> rand_core-0.9.5/src/lib.rs:300:1
|
300 | impl<R: TryRngCore> RngCore for UnwrapErr<R> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnwrapErr<R>`Read it carefully and the shape of the problem shows. The bound says rand_core::RngCore, and the list of types that do implement it comes from rand_core-0.9.5, the version we named. ThreadRng comes from rand 0.8, which implements the RngCore of rand_core 0.6.4. Same trait name, same module path, different crate. The compiler's text never mentions the version split, which is why this error costs people an afternoon. Note also how the generic version of the message misleads: it suggests DerefMut is missing, a red herring that comes from the compiler trying auto-dereference to make the call work.
Confirm it with cargo tree
cargo tree -drand_core v0.6.4
├── rand v0.8.8
│ └── dv v0.1.0 (/work/dv)
└── rand_chacha v0.3.1
└── rand v0.8.8 (*)
rand_core v0.9.5
└── dv v0.1.0 (/work/dv)The -d flag (--duplicates) prints only crates that appear at more than one version, and shows who depends on each. Two roots for rand_core is the diagnosis. Whenever a type error names a trait or type from a crate you know has several versions in your graph, run this first. For a larger graph, cargo tree -i [email protected] inverts the tree for that exact version so you see every path that holds it, and the cargo tree guide covers the filters.
Fix it, cheapest option first
The question to ask is: who should move? The answer is usually "whichever side is easy to align".
Align on the version your dependency uses. If you only named rand_core to get a trait, take it from the crate that owns the value. rand 0.8 re-exports rand_core, so the cleanest fix removes the direct dependency and uses the re-export, guaranteeing the same copy:
use rand::RngCore;
fn roll(rng: &mut impl RngCore) -> u32 {
rng.next_u32() % 6 + 1
}
fn main() {
let mut rng = rand::thread_rng();
println!("{}", roll(&mut rng));
}Re-exports are the library author's way of keeping versions in step, and a crate that exposes types from another in its API should always re-export that crate for this reason. If it does not, you must match versions by hand.
Upgrade the holder. If the old copy comes from a dependency that lags (rand 0.8 in this case), moving to rand 0.9 brings its own rand_core 0.9 into line and the duplicate disappears. Check cargo outdated for what the newer release changed, because 0.x upgrades are breaking by definition, and see keeping crates fresh for the routine. If an upgrade is blocked, tighten your own requirement to the old version: rand_core = "0.6".
Patch the holder. When a transitive dependency will not move and you need one copy, a patch pointing at a newer fork can unify them, with the usual caveat that it is temporary.
Duplicates that are harmless but not free
Not every duplicate causes a type error. If the two copies never meet, because no value of one crosses into the other, the build works and you pay in compile time and binary size: two copies of syn mean two compilations of one of the slowest crates in the ecosystem. Common duplicate pairs in real projects are syn 1 and 2, bitflags 1 and 2, and hashbrown at several versions. You can measure the cost with cargo build --timings and read the per-crate bars. A cheap guard against accumulating them is a policy check in CI: cargo deny has a multiple-versions setting under [bans] that warns or denies duplicates, with a skip list for the ones you have accepted, as set up in audit and deny. Deny is stricter than most teams can sustain; "warn" plus a periodic cleanup is the realistic setting.
Prevent it in your own libraries
If you publish a library whose public API mentions types from another crate, the duplicate problem lands on your users. Re-export the crate (pub use rand_core;), or accept a wide version range where the dependency's major versions stay compatible, or hide the foreign type behind your own. Choosing a looser requirement such as rand_core = ">=0.6, <0.10" is only valid if your code compiles against all of them, which you must test, for instance with cargo update --precise in CI. The wider discussion of what is public API is in public API design, and the lockfile angle in failed to select a version. And workspaces avoid most duplicates inside one repository by declaring versions once in workspace dependencies.
Seeing what one version costs
To decide whether a duplicate is worth removing, measure it. cargo build --timings writes an HTML report listing every crate with its compile time, and a duplicated crate appears as two bars:
cargo clean && cargo build --timings
cargo tree -d --depth 0 | grep -v '^$'Open target/cargo-timings/cargo-timing.html and look for the same name twice. For syn at two major versions, the second copy typically adds on the order of a second or two of wall time on a fast machine and more on CI, mostly because it sits on the critical path for every crate that uses derive macros. If the duplicates are not on the critical path, removing them saves total CPU but not wall time, and the work is not urgent. The second command lists only the duplicated crate names, which is a short checklist to attack from the largest down. Each removal is usually one dependency upgrade, so batch them into a single "dependency hygiene" pull request, and let cargo deny keep the count from growing back.
When to leave it alone
If cargo tree -d shows duplicates and nothing fails, you do not have a bug. Spending a day to unify bitflags that a deep dependency pins is poor use of time unless the compile time or binary size measurably matters. The cases to act on are the type errors above and any duplicate of a crate with global state (a logger, a runtime, once_cell style registries), where two copies mean two independent states and subtle misbehavior that no compiler error will flag.



