Skip to content

pnpm audit extremely slow (4+ min) and uncancellable after v11 rewrite #12086

Description

@aqeelat

pnpm version

11.5.0

Problem

After the v11 audit rewrite (commit ff28085997), pnpm audit takes 4+ minutes on large monorepos and cannot be cancelled with Ctrl+C.

Timing from our repo:

pnpm audit  251.46s user 2.22s system 93% cpu 4:30.94 total

93% CPU utilization confirms this is CPU-bound, not network-bound.

Root Cause

The v11 rewrite switched from npm's /audits/quick endpoint (server-side path resolution) to the /advisories/bulk endpoint. This moved path enumeration to the client side via walkForPaths() in deps/compliance/audit/src/lockfileToAuditIndex.ts:186.

Issue 1: Unbounded exponential graph traversal

walkForPaths() intentionally disables global visited-set deduplication to enumerate every distinct path to vulnerable packages. In a diamond dependency graph where a shared transitive dependency (e.g. lodash) is reachable through N parent chains, the function visits it N times.

While MAX_PATHS_PER_FINDING = 100 (line 240) caps the output, the traversal itself is not bounded — the recursive visit() calls on children (lines 211-218) still execute even when the parent's path was already capped. The early return at line 255 only skips recordPath(), not the remaining graph exploration.

The audit performs 6+ full synchronous graph traversals before any network request:

  1. detectDepTypes() — 3 traversals (dev, optional, prod subgraphs)
  2. collectOptionalOnlyDepPaths() — 2 traversals (with/without optional)
  3. lockfileToAuditRequest() — 1 traversal (POST body)
  4. buildAuditPathIndex() — 1 traversal WITHOUT dedup (the bottleneck)
  5. Steps 1-4 repeated for env lockfile if present

Issue 2: Ctrl+C doesn't work

All traversals are synchronous recursive functions (walkForPaths, detectDepTypes, collectOptionalOnlyDepPaths) that block Node.js's event loop. SIGINT cannot be processed until they complete. There are no AbortController/AbortSignal wired through the audit pipeline, and no setImmediate() yields.

Possible Fixes

  1. Short-circuit visit() when all vulnerable packages have hit MAX_PATHS: Track per-name path counts in visit() so it can skip subtrees that only lead to already-saturated vulnerable packages.

  2. Prune subtrees that don't contain vulnerable packages: If none of a node's descendants are in vulnerableNames, skip the subtree entirely. This requires a pre-computed "reachability set" (one additional walk with dedup, but then walkForPaths becomes near-linear).

  3. Yield to the event loop periodically: Insert await new Promise(r => setImmediate(r)) every N nodes to keep the process responsive to SIGINT.

  4. Wire an AbortController to process signals and cancel the traversal + network request.

Reproduction

Run pnpm audit on any large monorepo with thousands of dependencies. The issue scales with graph size and diamond dependency density.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions