When Enabling One Cargo Feature Changes Another Dependency's Behavior
A diagnostic guide for Rust workspaces where enabling a feature in one crate silently changes another crate’s behavior through Cargo’s feature unification.
20 Jun 2026, 06:00 UTC

Your crate compiles, but a struct gained an extra field, a trait implementation appeared without a local edit, or a runtime code path activated after you enabled a feature in an unrelated dependency. The useful takeaway: Cargo does not compile each dependency in isolation. It resolves one version per crate and unifies the union of all requested features across the entire graph. If two workspace members request different features of the same shared dependency, every requester sees the combined feature set.
Recognizable condition
You are likely looking at feature unification when one or more of these symptoms appear:
- Extra trait implementations show up in compiler errors or documentation, even though your code did not add them.
- Struct sizes or memory layouts change without any local edit.
- Runtime behavior toggles after adding a dependency that seems unrelated to the changed code path.
- Debug and release builds diverge because target-specific or optional dependencies resolve differently.
Cause and diagnostic table
| Symptom | Likely cause | First diagnostic |
|---|---|---|
| Extra trait impls appear | Unified optional features on a shared dependency | cargo tree -e normal,build -f '{p} {f}' |
| Behavior changes after adding an unrelated dep | New dependency requested a feature already active elsewhere | Diff unified feature sets before and after |
| Debug/release divergence | Target-specific or optional dependencies resolved differently | Inspect cargo tree -e all and target sections |
Unexpected cfg values | build.rs emitted configuration based on unified features | Check build script output and cargo tree for build-dependencies |
Ordered checks
Record the baseline unified feature set. Run from the workspace root with no elevated permissions:
cargo tree -e normal,build -f '{p} {f}' > baseline.txt. Expected check: a list of packages and their active features. Risk: the output format and available flags have changed across Cargo releases; confirm them againstcargo --version.Apply the suspect change (add the dependency or enable the feature), then rerun:
cargo tree -e normal,build -f '{p} {f}' > after.txtanddiff baseline.txt after.txt. Expected check: new lines showing features on the shared crate. Placeholder: replace<feature>with the feature name you enabled.Confirm the resolver and edition. In the workspace root
Cargo.toml, verifyresolver = "2"andedition = "2021"or later. Runcargo --version. Expected check: resolver v2 isolates build-dependency and proc-macro features from normal builds. Risk: older toolchains may not support resolver v2.Look for missing
default-features = false. Search everyCargo.tomlfor dependencies on the shared crate that omit this key. Expected check: explicit control over which features are active.Rule out overrides. Check for
[patch],[replace], and path dependencies that bypass normal version resolution.
Concrete workspace example
Consider a workspace with two members. In app/Cargo.toml:
[dependencies]
shared-dep = { version = "1.0", features = ["extra-impls"] }
In lib/Cargo.toml:
[dependencies]
shared-dep = { version = "1.0", default-features = false }
Because Cargo unifies features, lib still sees extra-impls active. The cargo tree diff will show that feature on shared-dep even though lib did not request it.
Fixes tied to findings
If unification is confirmed and you are on resolver v1
Set resolver = "2" in the workspace root Cargo.toml. This prevents build-dependency and proc-macro features from leaking into normal builds. It does not allow two versions of the same crate to coexist.
If a transitive feature is active because another crate requested it
Add a direct dependency on the shared crate with default-features = false and explicitly list only the features you need. Example configuration:
[dependencies]
shared-dep = { version = "1.0", default-features = false, features = ["io"] }
This documents your intent and prevents accidental default features, though it does not remove features requested by other dependents.
If a specific transitive release introduced a feature
Temporarily pin the version: cargo update -p shared-dep --precise 1.2.3. Run from the workspace root. Expected check: the suspect feature disappears from cargo tree. Risk: this can conflict with other dependents' semver requirements and may block security updates; treat it as a temporary diagnostic step.
If your own crate exposes optional functionality
Gate it behind explicit features in Cargo.toml rather than relying on a dependency's default features.
Escalation criteria
- The feature cannot be disabled upstream without losing required functionality.
- A proc-macro or build-dependency still leaks into normal builds under resolver v2.
- Behavior differs across toolchain versions the workspace cannot control.
When escalating upstream, include the output of cargo tree -e normal,build -f '{p} {f}' and the exact Cargo.lock.
Limitations and practical verification
Feature unification is resolver behavior, not a compiler guarantee. Tests passing locally can fail in CI where the resolved dependency graph differs. Do not assume feature sets are stable across cargo update; a new transitive release can add a feature and change unified behavior with no local edit.
resolver = "2" is not a universal fix. It changes build-dependency and proc-macro unification but does not allow two versions of the same crate to coexist. Pinning with --precise can conflict with other dependents' semver requirements and may block security updates.
Practical check: run cargo build --locked in CI so the verified Cargo.lock, not a freshly resolved graph, is what gets diagnosed. For a local scratch test, create a temporary crate depending only on the suspect dependency with default-features = false, add features one at a time, and note when behavior changes.
These steps assume a Cargo workspace on the 2021 edition or later. Resolver v2 semantics and cargo tree output format have changed across Cargo releases, so confirm flags and columns against the installed toolchain before relying on them.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.