Sonarqube scanner perf degradation after switching to pnpm

Both this and one other thread never got a follow-up so I decided to investigate this since we are running into the same issue here.

Solution: Remove node_modules before scanning in pnpm monorepos

We’ve been experiencing the same issue after migrating our large Nx monorepo (~200+ packages) from Yarn to pnpm. Here’s what we found and how we solved it.

The Problem

After switching to pnpm, our SonarCloud CI jobs started showing:

  • Symlink loop warnings during analysis
  • Significantly slower scan times
  • Occasional OOM kills in CI

The root cause is that pnpm’s node_modules structure uses symlinks extensively. When you have a package at packages/my-app/ with sonar.sources=., the scanner follows symlinks inside packages/my-app/node_modules/.pnpm/ which contains thousands of nested symlinked packages.

Even with sonar.exclusions=**/node_modules/** configured, the scanner appears to traverse these directories before applying exclusions, causing the symlink loop warnings.

Our Solution

Since SonarQube/SonarCloud only needs:

  1. Source code (already in git)
  2. Coverage reports (generated by test jobs)

It does not need node_modules at all. We simply delete each package’s node_modules directory immediately before running the scanner.

For Nx monorepos, we do this inside a custom executor so it happens after Nx builds its project graph but before sonar runs:

// packages/nx-helpers/src/executors/sonarqube/executor.ts
import { existsSync, rmSync } from 'fs';
import { join } from 'path';

export default async function runExecutor(options, context) {
  const projectRoot = /* ... get from context ... */;

  // Remove node_modules to prevent sonar from traversing pnpm symlinks
  // This is safe because sonar only needs source code and coverage reports
  const nodeModulesPath = join(context.root, projectRoot, 'node_modules');
  if (existsSync(nodeModulesPath)) {
    console.log(`Removing ${nodeModulesPath} to speed up sonar analysis...`);
    rmSync(nodeModulesPath, { recursive: true, force: true });
  }

  // Then run sonar-scanner
  const command = `pnpm sonar -Dsonar.token=... -Dsonar.organization=... ...`;
  // ...
}

Why not just use exclusions?

We tried several exclusion approaches:

  1. sonar.exclusions=**/node_modules/** - Still causes symlink traversal warnings, suggesting the scanner walks the filesystem before applying exclusions.

  2. More specific sonar.sources - Not practical in a large monorepo where each package has different source directories.

Results

  • No more symlink loop warnings
  • Faster scan times (less filesystem to traverse)
  • No OOM issues
  • Root node_modules preserved (needed for nx and @sonar/scan CLI)

Environment

  • pnpm 10.x
  • sonar/scan 4.3.2
  • SonarCloud
  • Nx 22.x monorepo

Suggestion for SonarSource

It would be helpful if the scanner could:

  1. Skip symlink traversal entirely for excluded paths (don’t even readdir them)
  2. Have a sonar.skipSymlinks=true option to avoid following symlinks
  3. Better document pnpm-specific configuration for monorepos

Hope this helps others hitting the same issue!