Lighthouse CI Troubleshooting Guide
Common issues when running Lighthouse CI and how to fix them.
Recommended Chrome Flags for CI
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
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:
- Disable Bot Fight Mode (Cloudflare) — it's meant for active attacks only
- Test against origin — bypass CDN entirely for CI testing
- Whitelist CI runner IPs — but be aware IPs change on shared runners
- Test staging environments without bot protection enabled
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:
| Source | Impact |
|---|---|
| Page nondeterminism (A/B tests, ads) | High |
| Local network variability | High |
| Client hardware variability | High |
| Client resource contention | High |
| Tier-1 network variability | Medium |
| Browser nondeterminism | Medium |
| Web server variability | Low |
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:
- Use dedicated CI runners (not shared)
- Minimum 2 cores and 4GB RAM — avoid burstable instances
- Use simulated throttling (default) for lowest variance
- 5 runs is 2x more stable than 1 run; 3 runs reduces variance by 37%
- Avoid testing on CI for precise performance measurements (consider real user monitoring instead)
Performance scores naturally vary ±5 points even on identical hardware. Focus on trends over time rather than individual runs.
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:
- Require PR authors to create issues instead of PRs (community contributions workflow)
- 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:
- Start with warnings only
- Collect 1 week of data
- Set error thresholds at P90 (90th percentile)
- Gradually lower thresholds
See GitHub Actions setup and GitLab CI setup for working examples.
Next Steps
Once your CI setup is working:
- Understand what you're measuring: Core Web Vitals overview explains the performance metrics
- Debug specific issues: Use LCP Finder, CLS Debugger, or INP Analyzer to diagnose failures
- Fix the issues: Dive into LCP, CLS, and INP optimization guides
- Set realistic thresholds: Use budgets to define performance standards without blocking every PR
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?
- Check GoogleChrome/lighthouse-ci GitHub Issues
- Review Lighthouse CI documentation
- Verify configuration reference syntax
- Enable verbose logging:
lhci autorun --verbose
- Recommended Chrome Flags for CI
- Chrome Launch Failures
- Bot Detection and 403 Errors
- Shallow Clone Issues
- Score Variance
- Upload Issues
- URL Normalization
- Lost Tokens
- Configuration Issues
- Memory and Timeout Issues
- startServerCommand Issues
- Assertion Failures
- Next Steps
- PROTOCOL_TIMEOUT Errors
- Chrome Headless Mode Changes
- Still Having Issues?