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:
detectDepTypes() — 3 traversals (dev, optional, prod subgraphs)
collectOptionalOnlyDepPaths() — 2 traversals (with/without optional)
lockfileToAuditRequest() — 1 traversal (POST body)
buildAuditPathIndex() — 1 traversal WITHOUT dedup (the bottleneck)
- 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
-
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.
-
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).
-
Yield to the event loop periodically: Insert await new Promise(r => setImmediate(r)) every N nodes to keep the process responsive to SIGINT.
-
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.
pnpm version
11.5.0
Problem
After the v11 audit rewrite (commit
ff28085997),pnpm audittakes 4+ minutes on large monorepos and cannot be cancelled with Ctrl+C.Timing from our repo:
93% CPU utilization confirms this is CPU-bound, not network-bound.
Root Cause
The v11 rewrite switched from npm's
/audits/quickendpoint (server-side path resolution) to the/advisories/bulkendpoint. This moved path enumeration to the client side viawalkForPaths()indeps/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 recursivevisit()calls on children (lines 211-218) still execute even when the parent's path was already capped. The early return at line 255 only skipsrecordPath(), not the remaining graph exploration.The audit performs 6+ full synchronous graph traversals before any network request:
detectDepTypes()— 3 traversals (dev, optional, prod subgraphs)collectOptionalOnlyDepPaths()— 2 traversals (with/without optional)lockfileToAuditRequest()— 1 traversal (POST body)buildAuditPathIndex()— 1 traversal WITHOUT dedup (the bottleneck)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 noAbortController/AbortSignalwired through the audit pipeline, and nosetImmediate()yields.Possible Fixes
Short-circuit
visit()when all vulnerable packages have hit MAX_PATHS: Track per-name path counts invisit()so it can skip subtrees that only lead to already-saturated vulnerable packages.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 thenwalkForPathsbecomes near-linear).Yield to the event loop periodically: Insert
await new Promise(r => setImmediate(r))every N nodes to keep the process responsive to SIGINT.Wire an
AbortControllerto process signals and cancel the traversal + network request.Reproduction
Run
pnpm auditon any large monorepo with thousands of dependencies. The issue scales with graph size and diamond dependency density.