Lighthouse CI Troubleshooting Guide

Fix common Lighthouse CI issues: Chrome launch failures, score variance, shallow clone errors, upload problems, and more.
Harlan WiltonHarlan Wilton Published

Common issues when running Lighthouse CI and how to fix them.

Most CI Chrome issues can be solved with this flag combination:

settings: {
  chromeFlags: [
    '--no-sandbox', // Required in containers
    '--disable-setuid-sandbox', // Backup for setuid sandbox issues
    '--disable-dev-shm-usage', // Avoid /dev/shm size issues
    '--disable-gpu', // GPU not available in most CI
  ].join(' ')
}

Chrome Launch Failures

Chrome Not Found

Problem: Error: Unable to find Chrome or Chrome could not be found on the system

Solution: Install Chrome on your CI runner:

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y wget gnupg
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
sudo apt-get update
sudo apt-get install -y google-chrome-stable

# Or use puppeteer's Chrome
npx @puppeteer/browsers install chrome@stable

For GitHub Actions, use the pre-installed Chrome:

- name: Run Lighthouse CI
  run: npx lhci autorun
  env:
    CHROME_PATH: /usr/bin/google-chrome

Protocol Error: X.Y wasn't found

Problem: Protocol error (X.Y): Z wasn't found or Chrome DevTools Protocol errors

Solution: Update to latest Lighthouse CI version (Chrome DevTools Protocol changes frequently):

npm install -D @lhci/cli@latest

If using an older Node version, you may need to upgrade Node.js to match the Chrome version installed on your system.

No usable sandbox

Problem: Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permitted or No usable sandbox!

Why this happens: Chrome's sandbox uses Linux user namespaces for isolation. Docker's default seccomp profile blocks these syscalls because the container already provides isolation — creating a conflict. On Ubuntu 24.04+, AppArmor restrictions add another layer requiring explicit profiles.

Solution: Add --no-sandbox flag in CI environments without proper sandboxing:

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      chromePath: process.env.CHROME_PATH,
      settings: {
        chromeFlags: '--no-sandbox --disable-gpu'
      }
    }
  }
}

Warning: Only use --no-sandbox in trusted CI environments running your own code. If auditing untrusted sites, malicious pages could escape. For maximum security, use Jess Frazelle's Chrome seccomp profile.

Browser Crashes in Docker (TARGET_CRASHED)

Problem: Browser tab has unexpectedly crashed or TARGET_CRASHED errors in Docker

Why this happens: Docker's default 64MB /dev/shm is too small for Chrome's shared memory requirements when rendering large pages.

Solution: Chrome needs adequate shared memory. Add --disable-dev-shm-usage flag (writes to disk instead, slower):

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      settings: {
        chromeFlags: '--no-sandbox --disable-dev-shm-usage --disable-gpu'
      }
    }
  }
}

Or mount a larger /dev/shm volume when running Docker (better performance):

docker run --shm-size=2g your-image
Docker Desktop with WSL2 has an additional issue where /dev/shm may lack execution permissions.

Bot Detection and 403 Errors

Problem: 403 Forbidden errors when auditing production sites with bot protection

Why this happens: Lighthouse's automated Chrome requests score 1-29 on Cloudflare's bot score, triggering blocks. Bot Fight Mode specifically blocks Lighthouse and can cause TBT to spike.

Solution: Sites with Cloudflare, WAFs, or bot detection may block Lighthouse's requests.

Options:

  1. Disable Bot Fight Mode (Cloudflare) — it's meant for active attacks only
  2. Test against origin — bypass CDN entirely for CI testing
  3. Whitelist CI runner IPs — but be aware IPs change on shared runners
  4. Test staging environments without bot protection enabled
Skip rules don't work for Super Bot Fight Mode. You must disable SBFM entirely or test a different endpoint.

This cannot be fixed in Lighthouse CI configuration—it requires changes on the target site's security settings.

Shallow Clone Issues

Ancestor hash not found

Problem: Could not find hash XXXXXX or fatal: bad object when comparing branches

Solution: Increase fetch depth in GitHub Actions:

- uses: actions/checkout@v4
  with:
    fetch-depth: 0 # Full history (safest)
    # Or use minimum depth for faster checkout:
    # fetch-depth: 20

For GitLab CI:

variables:
  GIT_DEPTH: 0

Lighthouse CI needs git history to compare against base branch commits. See GitHub Actions setup for complete configuration.

Score Variance

Inconsistent scores across runs

Problem: Performance scores vary by 5-15 points between identical runs

Why this happens: Google identifies 7 primary sources of variability:

SourceImpact
Page nondeterminism (A/B tests, ads)High
Local network variabilityHigh
Client hardware variabilityHigh
Client resource contentionHigh
Tier-1 network variabilityMedium
Browser nondeterminismMedium
Web server variabilityLow

Solution: Increase numberOfRuns and use consistent hardware:

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      numberOfRuns: 5, // Run 5 times, take median
      settings: {
        onlyCategories: ['performance'],
        // Disable CPU throttling variance
        throttling: {
          cpuSlowdownMultiplier: 1
        }
      }
    },
    assert: {
      assertions: {
        // Use ranges for flaky metrics
        'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
        'interactive': ['warn', { maxNumericValue: 5000 }]
      }
    }
  }
}

Best practices to reduce variance:

Performance scores naturally vary ±5 points even on identical hardware. Focus on trends over time rather than individual runs.

December 2024 change: PageSpeed Insights changed CPU throttling from 4x to 1.2x to better match production environments. If your PSI scores suddenly improved, this is why.

Understanding the metrics you're testing helps interpret variance. See guides for LCP, CLS, and INP.

Upload Issues

GitHub App not working on fork PRs

Problem: Status checks don't appear on pull requests from forks

Solution: This is a security limitation. Forks can't access repository secrets or GitHub Apps. Options:

  1. Require PR authors to create issues instead of PRs (community contributions workflow)
  2. Use pull_request_target with caution:
on:
  pull_request_target: # Runs in base repo context

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      # DANGER: Only checkout base branch, never fork code
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.base_ref }}

      - name: Run Lighthouse CI
        run: npx lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

Warning: pull_request_target runs with write access. Never checkout or execute fork code.

Token security for public repos

Problem: Don't want to expose server token in public repo

Solution: Use GitHub Secrets and the Lighthouse CI GitHub App:

env:
  LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

Never hardcode tokens in lighthouserc.js. For public repos, the GitHub App is safer than upload.token because it scopes permissions to status checks only.

URL Normalization

Dynamic URLs with ports or UUIDs

Problem: Each CI run creates different URLs (localhost:34567, preview-abc123.app.com) that can't be compared historically

Solution: Use --url-replacement-patterns to normalize URLs:

# Normalize port numbers
lhci collect --url-replacement-patterns='http://localhost:[0-9]+/=//'

# Normalize preview UUIDs
lhci collect --url-replacement-patterns='https://preview-[a-f0-9]+.app.com/=//'

Or in configuration:

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:8080'],
      urlReplacementPatterns: [
        's/localhost:[0-9]+/localhost:PORT/g',
        's/preview-[a-f0-9]+/preview-UUID/g'
      ]
    }
  }
}

This ensures historical data aggregates properly across different deployments.

Lost Tokens

Recovering Lost GitHub App Token

Problem: Lost GitHub App token after initial setup

Solution: Uninstall and reinstall the Lighthouse CI GitHub App on your repository to get a new token.

Recovering Lost Build Token (LHCI Server)

Problem: Lost build token for uploading to your LHCI server

Solution: Reset the build token via wizard:

# Connect to your LHCI server and run:
lhci wizard --wizard=reset-build-token

Or create a new project via API if you have admin token:

curl -X POST https://your-lhci-server.com/v1/projects \
  -H "Authorization: Bearer $LHCI_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-project", "externalUrl": "https://github.com/user/repo"}'

Admin vs Build tokens

  • Admin token: Full server access, manage projects. Keep secret, never commit.
  • Build token: Upload-only for specific project. Safe in CI secrets.
  • GitHub App token: Status check posting only. Safest for public repos.

Configuration Issues

lighthouserc.js not found

Problem: Could not find configuration file

Solution: Ensure file is in project root and properly named:

# Must be one of these names
lighthouserc.js
lighthouserc.json
.lighthouserc.js
.lighthouserc.json

# Or specify explicitly
lhci autorun --config=./path/to/lighthouserc.js

Invalid configuration format

Problem: Configuration parsing failed or Invalid config

Solution: Validate syntax. Configuration must export an object with ci key:

// ✅ Correct
module.exports = {
  ci: {
    collect: { /* ... */ }
  }
}

// ❌ Wrong - missing 'ci' wrapper
module.exports = {
  collect: { /* ... */ }
}

See configuration reference for complete schema.

Memory and Timeout Issues

Out of memory on large sites

Problem: JavaScript heap out of memory or CI runner killed

Solution: Increase Node memory and reduce concurrent audits:

# Increase Node memory
NODE_OPTIONS=--max-old-space-size=4096 lhci autorun
// lighthouserc.js - reduce load
module.exports = {
  ci: {
    collect: {
      numberOfRuns: 3, // Reduce from 5
      url: [
        // Audit fewer pages
        'http://localhost:8080',
        'http://localhost:8080/about'
      ]
    }
  }
}

For large sites (50+ pages), consider:

  • Split audits across multiple jobs
  • Use more powerful CI runners
  • Audit subset of critical pages only

CI timeout on slow runners

Problem: CI job exceeds time limit (30min default on GitHub Actions)

Solution: Optimize collection or increase timeout:

jobs:
  lighthouse:
    timeout-minutes: 60 # Default is 360 (6h), reduce for faster failure detection
// lighthouserc.js - faster collection
module.exports = {
  ci: {
    collect: {
      numberOfRuns: 3, // Reduce runs
      settings: {
        onlyCategories: ['performance'], // Single category
        skipAudits: ['screenshot-thumbnails', 'final-screenshot'] // Skip slow audits
      }
    }
  }
}

startServerCommand Issues

Server not starting

Problem: startServerCommand runs but server never becomes available

Solution: Add explicit readiness check:

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      startServerCommand: 'npm run serve',
      startServerReadyPattern: 'Local:.*http://localhost:8080', // Wait for this log
      startServerReadyTimeout: 30000, // 30s timeout
      url: ['http://localhost:8080']
    }
  }
}

Common patterns:

  • Vite: Local:.*http://localhost
  • Next.js: ready on
  • Express: listening on

Wrong port

Problem: Server starts on random port, hardcoded URL fails

Solution: Force specific port in start command:

# package.json
{
  "scripts": {
    "serve": "vite preview --port 8080 --strictPort"
  }
}

Or set port via environment variable:

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      startServerCommand: 'PORT=8080 npm run serve',
      startServerReadyPattern: 'http://localhost',
      url: ['http://localhost:8080']
    }
  }
}

Assertion Failures

Understanding error vs warn

Assertions can 'error' (fail build) or 'warn' (log only):

// lighthouserc.js
module.exports = {
  ci: {
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }], // Fail if < 90
        'first-contentful-paint': ['warn', { maxNumericValue: 2000 }], // Log if > 2s

        // Use warn for flaky metrics
        'speed-index': ['warn', { maxNumericValue: 4000 }],

        // Use error for critical issues
        'errors-in-console': ['error', { maxLength: 0 }]
      }
    }
  }
}

Setting realistic budgets

Problem: Assertions constantly fail, blocking PRs

Solution: Set budgets based on current performance, then gradually improve:

# Get baseline scores
lhci collect --url=http://localhost:8080
lhci assert --preset=lighthouse:no-pwa

# Set budgets 10% better than current
# If current performance score is 75, set minScore: 0.7

Start with preset: 'lighthouse:recommended' and relax failing assertions until stable, then tighten over time.

Recommended approach:

  1. Start with warnings only
  2. Collect 1 week of data
  3. Set error thresholds at P90 (90th percentile)
  4. Gradually lower thresholds

See GitHub Actions setup and GitLab CI setup for working examples.

Next Steps

Once your CI setup is working:

PROTOCOL_TIMEOUT Errors

Problem: Chrome hangs during audit with PROTOCOL_TIMEOUT error

Solution: This is a known unfixable issue — Chrome occasionally hangs and nothing can be done on the Lighthouse side. The only solution is to retry:

// In CI, wrap with retry logic
// Or increase numberOfRuns and accept occasional failures
module.exports = {
  ci: {
    collect: {
      numberOfRuns: 5, // More runs = better chance of success
    }
  }
}

Chrome Headless Mode Changes

Problem: Deprecation warnings about --headless or crashes with Chrome 132+

Solution: Chrome 132 removed --headless=old from the main binary. LHCI 0.14+ uses headless: "new" by default. If you need old headless behavior, use the separate chrome-headless-shell binary.

Still Having Issues?