Recommended Free Tools
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Puppeteer failures launched by PHP are usually boundary failures, not mysterious browser bugs. PHP must start the correct Node process, that process must load the intended Puppeteer package and browser, and Chromium must be able to start and complete the page operation under the web server’s account. Capture the full error, classify which boundary failed, then reproduce the smallest failing step as the same account. This method fixes missing Chrome, empty PHP output, PHP-FPM-only launch failures, and navigation timeouts without masking the original cause.
Start by identifying the failing boundary
There are three separate hand-offs:
- PHP to Node: PHP finds the executable, sets the working directory and environment, starts the child, reads both output streams, and records its exit code.
- Node to Puppeteer: Node resolves the package, cache, browser path, and launch options.
- Puppeteer to Chromium and the page: Chromium starts, creates a profile, navigates, and performs selectors, screenshots, or PDFs.
Fix the earliest failed hand-off. A browser navigation timeout cannot be repaired by changing PHP’s PATH, and a missing Node executable cannot be repaired with Chromium flags.
| First symptom | Likely boundary | First check |
|---|---|---|
| PHP returns no output | PHP-to-Node transport | Node path, working directory, stderr capture, and child exit code |
Cannot find module 'puppeteer' |
Node-to-Puppeteer | Which Node installation and project directory the service account uses |
| Could not find Chrome | Browser installation/cache | Browser install, cache ownership, and resolved executable path |
Failed to launch the browser process |
Chromium startup | dumpio output, dependencies, permissions, sandbox, and writable profile paths |
| Navigation or selector timeout | Page operation | URL reachability, wait strategy, timeout value, frame, and selector state |
Preserve evidence before changing settings
Record the complete error and stack trace, Node.js version, Puppeteer version, browser version, exact operation, command arguments (with secrets removed), process exit status, and stderr. Keep stdout machine-readable so PHP does not mistake a diagnostic line for a successful result. Send logs to stderr.
Free tools Windows power users keep installed
One-click scans. No signup required.
Run the Node script directly as the same Unix account used by Apache, PHP-FPM, a queue worker, CI, or the container. Print these values during diagnosis:
#1 Best Overall
process.version- Puppeteer’s package version
- the browser version
process.cwd()andprocess.env.HOME- the resolved browser executable path
If the command works in your shell but fails through PHP, compare the two environments rather than assuming they are equivalent.
Build a minimal Node bridge first
Remove navigation, selectors, PDF generation, and screenshots until launch alone succeeds. This bridge emits one JSON object on stdout and sends Chromium diagnostics to stderr:
const puppeteer = require('puppeteer');
const target = process.argv[2];
if (!target) {
console.error('usage: node capture.js https://example.com');
process.exit(2);
}
(async () => {
let browser;
try {
console.error(JSON.stringify({
stage: 'diagnostic',
node: process.version,
cwd: process.cwd(),
home: process.env.HOME || null,
executable: puppeteer.executablePath()
}));
browser = await puppeteer.launch({
dumpio: true,
timeout: 30000,
// Set this only when you have verified a real executable path:
// executablePath: '/usr/bin/google-chrome',
userDataDir: process.env.PUPPETEER_USER_DATA_DIR || undefined
});
const page = await browser.newPage();
await page.goto(target, { waitUntil: 'networkidle2', timeout: 60000 });
const result = {
stage: 'complete',
url: page.url(),
title: await page.title()
};
process.stdout.write(JSON.stringify(result) + 'n');
} catch (error) {
process.stdout.write(JSON.stringify({
stage: browser ? 'page' : 'launch',
message: error.message,
stack: error.stack
}) + 'n');
process.exitCode = 1;
} finally {
if (browser) await browser.close().catch(() => {});
}
})();
Use a real writable directory for PUPPETEER_USER_DATA_DIR in production. The finally block prevents repeated PHP calls from leaving Chromium processes behind.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Have PHP capture stdout, stderr, and the exit code
shell_exec() hides too much for diagnosis. proc_open() lets PHP preserve both streams and enforce a deadline:
Rank #2
<?php
$url = $argv[1] ?? 'https://example.com';
$node = '/usr/bin/node';
$script = '/opt/app/capture.js';
$command = escapeshellarg($node) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($url);
$descriptors = [
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'] // stderr
];
$env = [
'PATH' => '/usr/local/bin:/usr/bin:/bin',
'HOME' => '/var/lib/www',
'PUPPETEER_CACHE_DIR' => '/var/lib/www/.cache/puppeteer',
'PUPPETEER_USER_DATA_DIR' => '/var/lib/www/puppeteer-profile'
];
$process = proc_open($command, $descriptors, $pipes, '/opt/app', $env);
if (!is_resource($process)) {
throw new RuntimeException('Could not start Node bridge');
}
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$stdout = '';
$stderr = '';
$deadline = microtime(true) + 90;
while (true) {
$stdout .= stream_get_contents($pipes[1]);
$stderr .= stream_get_contents($pipes[2]);
$status = proc_get_status($process);
if (!$status['running']) {
break;
}
if (microtime(true) > $deadline) {
proc_terminate($process);
break;
}
usleep(100000);
}
$stdout .= stream_get_contents($pipes[1]);
$stderr .= stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($process);
$result = json_decode(trim($stdout), true);
if (!is_array($result) || $exitCode !== 0) {
error_log(json_encode([
'stage' => $result['stage'] ?? 'transport',
'message' => $result['message'] ?? 'Node returned no valid JSON',
'stderr' => $stderr,
'exit_code' => $exitCode
]));
http_response_code(502);
exit('Puppeteer failed');
}
echo json_encode($result), PHP_EOL;
Replace the paths with locations that exist on the machine running Node. In a web request, do not expose raw stderr or command arguments to the client; log them securely and return a correlation ID instead. Redact tokens embedded in URLs, headers, cookies, or authorization arguments.
Repair browser installation and cache problems
Install for the runtime user
Since Puppeteer v19, downloaded browsers are stored under ~/.cache/puppeteer by default. A package-manager install script may be blocked in CI or a deployment image. Run npx puppeteer browsers install during the image build or deployment, then verify that the same service account can read and execute the cache.
For a shared build cache, set PUPPETEER_CACHE_DIR to a persistent directory. It must exist at runtime, be readable and executable by the PHP/Node account, and survive the boundary between a build stage and the final container. A stable HOME also prevents the cache from silently moving to a different user’s directory.
Check custom executable paths and versions
executablePath must name a browser inside the machine or container where Node runs. Test it as the service account, check execute permission, and inspect missing shared libraries. Puppeteer is only guaranteed to work with its bundled browser when a custom executable is selected; pin and test the browser/Puppeteer pair instead of assuming every system Chrome build is interchangeable.
Fix launch, sandbox, and filesystem failures
For Failed to launch the browser process, keep dumpio: true enabled and read the first meaningful Chromium stderr line. Common causes are missing Linux libraries, an incorrect executable path, sandbox permission errors, and insufficient privileges.
- Sandbox: A container or restricted service account may reject the sandbox.
--no-sandboxis an environment-specific workaround, not a universal repair; understand the security trade-off and prefer a correctly configured sandbox. - Read-only containers: Chromium writes profile, configuration, and cache data before Puppeteer connects. Provide writable XDG configuration/cache locations and an explicit writable
userDataDirowned by the runtime user. - Alpine: Chrome does not support Alpine out of the box. Match the Chromium package to a Puppeteer version that supports it and install the required packages. Alpine Chromium timeout behavior has changed between releases, so verify the current Alpine and Chromium versions rather than copying an old workaround.
Use a unique temporary profile for concurrent jobs. Reusing one profile across workers can create lock contention, corrupted state, or cross-request cookies. Delete temporary profiles after closing the browser.
Correct PHP-FPM, queue, and container environments
PHP-FPM often has a smaller PATH, a different HOME, another current directory, and fewer permissions than an interactive shell. Confirm that the FPM account can execute Node and Chromium, read the project’s node_modules, create its cache and profile directories, and reach the target network. Set the working directory explicitly in proc_open(); do not depend on the web server’s default directory.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsA bridge may start while the browser fails. Return structured fields such as stage, message, stderr, and exit_code so PHP can distinguish transport, launch, and page errors. For queued work, keep the worker alive until the Puppeteer promise settles. Some cloud runtimes suspend CPU after an HTTP response, which can terminate unfinished browser work; wait for completion or move it to a real worker.
Rank #4
Separate navigation and page-operation errors
Once launch works, add one operation at a time. For every failure, record the URL (with secrets redacted), navigation timeout, HTTP or security error, selector, frame, and whether the target element was replaced during a client-side render.
- Navigation timeout: Check DNS and outbound access from the runtime, then choose an appropriate
waitUntilcondition and page timeout. Long-lived analytics or streaming requests can makenetworkidle2unsuitable. - Selector timeout: Confirm the selector in the same viewport and wait for the correct frame or shadow-root context. A page transition may detach the element you found.
- Blank or partial output: Wait for the page-specific selector or a measured delay, and ensure lazy content has been triggered before capturing.
- Intermittent failures: Log attempt number, browser PID, URL, and elapsed times. Retry only transient navigation or infrastructure errors; do not blindly retry syntax, selector, or authentication failures.
Choose an architecture that matches the workload
| Architecture | Advantages | Risks and controls |
|---|---|---|
| Node process per PHP request | Simple isolation and easy PHP integration | Higher startup latency; enforce a timeout and reap children |
| Persistent Node service | Amortizes browser startup and centralizes versioning | Needs health checks, request isolation, back-pressure, and restart cleanup |
| Synchronous PHP wait | Immediate result for a user request | Consumes web workers; set an HTTP and process deadline |
| Queue and worker | Handles slow pages and bursts without blocking PHP-FPM | Requires job state, retries, idempotency, and artifact storage |
| Bundled browser | Known Puppeteer compatibility | Larger image/cache; persist and permission the cache |
| System browser | May already exist in the image | Version drift; pin and test the exact pair |
Performance, reliability, and cost controls
- Reuse a browser process only when jobs are isolated by context and you have limits for memory, pages, and lifetime.
- Use one page per job, close it in a
finallypath, and recycle workers after repeated crashes or a defined number of jobs. - Set separate launch, navigation, and overall job deadlines so a stalled page cannot consume a PHP-FPM worker forever.
- Persist browser downloads in CI, but never share a mutable profile between concurrent jobs.
- Measure launch time, navigation time, bytes transferred, retries, and failure stage. These measurements identify whether scaling Node workers or fixing the target site will help.
Common errors and precise fixes
| Error or symptom | Cause to verify | Fix |
|---|---|---|
| Could not find Chrome | Install script skipped, wrong cache, or cache owned by another user | Run npx puppeteer browsers install; set a persistent PUPPETEER_CACHE_DIR; test as the runtime account |
| Empty PHP response | stderr discarded, wrong PATH, bad directory, or child killed | Use proc_open(), capture both streams, set PATH/HOME/cwd, and record exit status |
| Works in shell, fails in PHP-FPM | Different user, environment, permissions, or network policy | Run the exact command as FPM’s account and compare environment values |
| Browser exits immediately | Missing libraries, sandbox denial, bad executable, or unwritable profile | Enable dumpio; inspect stderr; install dependencies; fix permissions or isolate a profile |
| Timeout only in Docker/CI | Missing dependencies, cold browser cache, Alpine mismatch, or restricted CPU/network | Install and persist dependencies/cache, verify the base image, and test from inside the final runtime |
| Selector or navigation timeout | Wrong wait condition, unreachable URL, frame change, or replaced element | Debug page operation separately; adjust waits and selectors after launch is proven |
Or skip the browser setup
If your requirement is simply a reliable website image or PDF from PHP, ScreenshotNeo provides a single HTTP endpoint instead of a local Chromium installation. The same request can return PNG, JPEG, WebP, or PDF:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for parameters. Python and Node.js callers can use the same endpoint:
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchimport requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account to get started.
FAQ
Should I return Chromium stderr to the browser client?
No. Log it with a request ID and return a sanitized error. Browser stderr can contain URLs, file paths, or credentials accidentally included in command arguments.
When is a persistent Node service preferable?
Use one when launch overhead dominates and you can enforce browser-context isolation, worker limits, health checks, and recycling. Keep a per-request process when isolation and operational simplicity matter more than startup time.
How should I handle retries?
Retry only classified transient failures, with a small capped backoff and an idempotent job key. A missing executable, invalid selector, or authentication error needs a configuration fix, not repeated attempts.
Frequently Asked Questions
Can I use a system Chrome binary with any Puppeteer version?
No. Puppeteer documents compatibility guarantees for its bundled browser; a custom executable should be pinned and tested with the exact Puppeteer version you deploy.
Why does a read-only container fail before my script opens a page?
Chromium needs writable profile, configuration, and cache locations during startup. Provide writable XDG paths and an owned userDataDir, or use an image with an appropriate filesystem.
What is the safest way to run many simultaneous captures?
Use isolated browser contexts or temporary profiles, cap concurrency, enforce launch and job deadlines, and close every page and browser in cleanup code.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →

