Skip to content

-Z cargo-lints can make Cargo retain >100 GiB on large workspace #17154

Description

@enricoschaaf

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

  1. 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
  2. Run:

    cargo check --workspace --all-targets --all-features -Z cargo-lints
  3. 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]

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-diagnosticsArea: Error and warning messages generated by Cargo itself.C-bugCategory: bugPerformanceGotta go fast!S-needs-infoStatus: Needs more info, such as a reproduction or more background for a feature request.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions