Problem
Running cargo check --workspace --all-targets --all-features -Z cargo-lints on a large private workspace caused the Cargo process itself to retain extremely high memory.
In CI, the same lint path without -Z cargo-lints uses around 11 GiB container memory. With -Z cargo-lints, this command reached around 130 GiB container memory, with one Cargo process around 128 GiB RSS.
This appears to be Cargo-process memory, not rustc/clippy/linker memory, and not jobserver/build parallelism. The high-RSS process was a single Cargo process running:
cargo check --message-format json-diagnostic-rendered-ansi --workspace --all-targets --all-features -Z cargo-lints
Expected behavior: enabling cargo-lints should add roughly bounded overhead relative to the workspace/unit graph size, not retain memory proportional to the number of paths through the dependency graph.
This likely only becomes visible at larger graph sizes, but it should not require a workspace this large to reproduce. Larger workspaces should mainly accelerate or amplify the issue. The unit graph for the affected command has roughly 2.8k units and 20k unit dependency edges. Because the suspicious is_transitive_dep implementation walks the unit graph without a visited set, shared dependency DAGs can plausibly amplify this into path-count behavior.
Steps
-
Use a Rust workspace with a sufficiently large/shared dependency graph. The affected private workspace has roughly:
- 170 workspace members
- 1.8k resolved packages
- 9.7k package dependency edges
- 2.8k Cargo units for
cargo check --workspace --all-targets --all-features
- 20k unit dependency edges
- 470 unit graph roots
-
Run:
cargo check --workspace --all-targets --all-features -Z cargo-lints
-
Observe Cargo process RSS.
A minimized reproducer may be substantially smaller than this if it creates enough fan-out/fan-in in the Cargo unit graph. The private workspace size above is included as scale context, not as a presumed lower bound for reproduction.
For A/B comparison, run the same command without -Z cargo-lints after clearing the same target cache scope.
I do not yet have a minimized public reproducer, but the behavior is reproducible in our CI environment.
Possible Solution(s)
One suspicious code path is the cargo::unused_dependencies transitive dependency check.
At Cargo a595d0da21f228b7fdae64d3d5c0e527ea66bb59, is_transitive_dep walks the unit graph using a queue but does not track visited units:
|
fn is_transitive_dep( |
|
direct_dep_unit: &Unit, |
|
seen_units: &Vec<Unit>, |
|
bcx: &BuildContext<'_, '_>, |
|
) -> bool { |
|
let mut queue = std::collections::VecDeque::new(); |
|
for root_unit in seen_units { |
|
for unit_dep in &bcx.unit_graph[root_unit] { |
|
if root_unit.pkg.package_id() == unit_dep.unit.pkg.package_id() { |
|
continue; |
|
} |
|
if unit_dep.unit == *direct_dep_unit { |
|
continue; |
|
} |
|
queue.push_back(&unit_dep.unit); |
|
} |
|
} |
|
|
|
while let Some(dep_unit) = queue.pop_front() { |
|
for unit_dep in &bcx.unit_graph[dep_unit] { |
|
if unit_dep.unit == *direct_dep_unit { |
|
return true; |
|
} |
|
queue.push_back(&unit_dep.unit); |
|
} |
|
} |
|
|
|
false |
|
} |
In a large DAG with many shared dependencies, this can revisit the same units through many different paths and grow work/memory with the number of paths rather than the number of unique units.
A possible fix would be to add a visited set to this traversal, and possibly cache reachability results across is_transitive_dep calls.
Notes
This path is enabled by -Z cargo-lints:
One detail that may matter: the unused-extern collection is enabled by the cargo-lints feature gate itself. Cargo currently passes --force-warn=unused_crate_dependencies and requests unused-externs-silent whenever cli_unstable().cargo_lints is true, even if the workspace did not explicitly configure or intend to run cargo::unused_dependencies. That means enabling -Z cargo-lints for other Cargo lints can still pay the unused-dependency collection cost.
-
Cargo forces rustc unused_crate_dependencies:
|
if build_runner.bcx.gctx.cli_unstable().cargo_lints { |
|
// Added last to reduce the risk of RUSTFLAGS or `[lints]` from interfering with |
|
// `unused_dependencies` tracking |
|
base.arg("--force-warn=unused_crate_dependencies"); |
|
} |
-
Cargo collects unused extern messages:
|
Message::UnusedExterns(id, unused_externs) => { |
|
let unit = &self.active[&id]; |
|
build_runner |
|
.unused_dep_state |
|
.record_unused_externs_for_unit(unit, unused_externs); |
-
Cargo runs post-build unused_dependencies::lint_build_results:
|
if build_runner.bcx.gctx.cli_unstable().cargo_lints { |
|
let mut global_stats = GlobalDiagnosticStats::new(); |
|
drop(unused_dependencies::lint_build_results( |
|
build_runner, |
|
&mut global_stats, |
|
)); |
|
errors.count += global_stats.error_count(); |
|
build_runner.compilation.lint_warning_count += global_stats.lint_warning_count(); |
Unit-graph validation for the affected command shape supports this hypothesis. The graph has 2771 units, 474 roots, and 169 packages. Replaying the no-visited traversal for one package/dependency pair hit a 1B enqueue cap (logical_max_queue=583267812, logical_enqueues=1002286320), while the same traversal with visited-unit deduplication stayed bounded (visited_max_queue=556, visited_seen=1999). This is consistent with repeated queuing through many dependency paths; pointer-sized queue entries alone would make the capped case about 4.35 GiB of logical queue storage before allocator overhead or repeated calls.
The suspicious is_transitive_dep implementation was introduced in 944a5403d and is still present on current master at the time of checking.
I searched for existing issues mentioning unused_dependencies memory, -Zcargo-lints memory, or is_transitive_dep, and did not find a duplicate for this exact behavior.
Version
cargo 1.98.0-nightly (a595d0da2 2026-06-20)
release: 1.98.0-nightly
commit-hash: a595d0da21f228b7fdae64d3d5c0e527ea66bb59
commit-date: 2026-06-20
host: x86_64-unknown-linux-gnu
libgit2: 1.9.4 (sys:0.21.0 vendored)
libcurl: 8.20.0-DEV (sys:0.4.89+curl-8.20.0 vendored ssl:OpenSSL/3.6.2)
ssl: OpenSSL 3.6.2 7 Apr 2026
os: Debian 13.0.0 (trixie) [64-bit]
Problem
Running
cargo check --workspace --all-targets --all-features -Z cargo-lintson a large private workspace caused the Cargo process itself to retain extremely high memory.In CI, the same lint path without
-Z cargo-lintsuses around 11 GiB container memory. With-Z cargo-lints, this command reached around 130 GiB container memory, with one Cargo process around 128 GiB RSS.This appears to be Cargo-process memory, not rustc/clippy/linker memory, and not jobserver/build parallelism. The high-RSS process was a single Cargo process running:
Expected behavior: enabling cargo-lints should add roughly bounded overhead relative to the workspace/unit graph size, not retain memory proportional to the number of paths through the dependency graph.
This likely only becomes visible at larger graph sizes, but it should not require a workspace this large to reproduce. Larger workspaces should mainly accelerate or amplify the issue. The unit graph for the affected command has roughly 2.8k units and 20k unit dependency edges. Because the suspicious
is_transitive_depimplementation walks the unit graph without a visited set, shared dependency DAGs can plausibly amplify this into path-count behavior.Steps
Use a Rust workspace with a sufficiently large/shared dependency graph. The affected private workspace has roughly:
cargo check --workspace --all-targets --all-featuresRun:
Observe Cargo process RSS.
A minimized reproducer may be substantially smaller than this if it creates enough fan-out/fan-in in the Cargo unit graph. The private workspace size above is included as scale context, not as a presumed lower bound for reproduction.
For A/B comparison, run the same command without
-Z cargo-lintsafter clearing the same target cache scope.I do not yet have a minimized public reproducer, but the behavior is reproducible in our CI environment.
Possible Solution(s)
One suspicious code path is the
cargo::unused_dependenciestransitive dependency check.At Cargo
a595d0da21f228b7fdae64d3d5c0e527ea66bb59,is_transitive_depwalks the unit graph using a queue but does not track visited units:cargo/src/cargo/diagnostics/rules/unused_dependencies.rs
Lines 384 to 412 in a595d0d
In a large DAG with many shared dependencies, this can revisit the same units through many different paths and grow work/memory with the number of paths rather than the number of unique units.
A possible fix would be to add a visited set to this traversal, and possibly cache reachability results across
is_transitive_depcalls.Notes
This path is enabled by
-Z cargo-lints:One detail that may matter: the unused-extern collection is enabled by the
cargo-lintsfeature gate itself. Cargo currently passes--force-warn=unused_crate_dependenciesand requestsunused-externs-silentwhenevercli_unstable().cargo_lintsis true, even if the workspace did not explicitly configure or intend to runcargo::unused_dependencies. That means enabling-Z cargo-lintsfor other Cargo lints can still pay the unused-dependency collection cost.Cargo forces rustc
unused_crate_dependencies:cargo/src/cargo/core/compiler/mod.rs
Lines 840 to 844 in a595d0d
Cargo collects unused extern messages:
cargo/src/cargo/core/compiler/job_queue/mod.rs
Lines 748 to 752 in a595d0d
Cargo runs post-build
unused_dependencies::lint_build_results:cargo/src/cargo/core/compiler/job_queue/mod.rs
Lines 846 to 853 in a595d0d
Unit-graph validation for the affected command shape supports this hypothesis. The graph has
2771units,474roots, and169packages. Replaying the no-visited traversal for one package/dependency pair hit a 1B enqueue cap (logical_max_queue=583267812,logical_enqueues=1002286320), while the same traversal with visited-unit deduplication stayed bounded (visited_max_queue=556,visited_seen=1999). This is consistent with repeated queuing through many dependency paths; pointer-sized queue entries alone would make the capped case about 4.35 GiB of logical queue storage before allocator overhead or repeated calls.The suspicious
is_transitive_depimplementation was introduced in944a5403dand is still present on currentmasterat the time of checking.I searched for existing issues mentioning
unused_dependenciesmemory,-Zcargo-lintsmemory, oris_transitive_dep, and did not find a duplicate for this exact behavior.Version