Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When Puppeteer works in Node.js but fails through PHP, find the first failing boundary: PHP starting Node, Node loading Puppeteer, or Puppeteer launching Chromium and handling the page. Run the same minimal Node script as the PHP service account, capture both output streams and the exit code, then fix that first failure before adding screenshots, PDFs, or selectors.

Identify which boundary is failing

PHP does not run Puppeteer itself. In a typical setup, PHP starts a Node.js script (or sends work to a Node service); that Node process loads Puppeteer; Puppeteer then starts Chromium and performs the requested page operation. An error at one boundary can look like an error at another, so start with the first real failure rather than changing browser flags at random.

  • PHP-to-Node bridge: Node cannot start, the working directory is wrong, required environment variables are missing, or PHP cannot read the child process output.
  • Node-to-browser launch: Puppeteer cannot find or execute the browser, the browser exits immediately, or the runtime lacks permissions or system libraries.
  • Page operation: The browser launches, but navigation, a selector, a frame, or a later operation fails or times out.

Before changing anything, save the complete error and stack trace; Node, Puppeteer, and browser versions; the exact operation and arguments; the child exit status; and both stdout and stderr. Redact credentials and sensitive URL parameters before retaining logs.

Reproduce the failure as the PHP service account

A command that succeeds in your interactive shell may fail under Apache, PHP-FPM, a queue worker, CI, or a container. Those processes can run as a different user and have a different PATH, HOME, working directory, cache, permissions, and environment. Run a small Node diagnostic using the same account and runtime that PHP uses.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Check Node, Puppeteer, and the browser

Save this as diagnose.cjs in the directory containing the installed puppeteer package. It prints runtime details to stderr so stdout remains available for machine-readable results.

const puppeteer = require('puppeteer');

(async () => {
  console.error(JSON.stringify({
    node: process.version,
    puppeteer: require('puppeteer/package.json').version,
    cwd: process.cwd(),
    home: process.env.HOME || process.env.USERPROFILE || null,
    browserPath: puppeteer.executablePath()
  }, null, 2));

  const browser = await puppeteer.launch({
    headless: true,
    dumpio: true
  });
  try {
    console.error('Browser version:', await browser.version());
    const page = await browser.newPage();
    await page.goto('about:blank');
    console.log(JSON.stringify({ ok: true, stage: 'page-opened' }));
  } finally {
    await browser.close();
  }
})().catch(error => {
  console.error(error.stack || error);
  process.exitCode = 1;
});

Invoke it with the same Node executable, user, working directory, and environment as the failing PHP job. If this minimal launch fails, do not debug your page URL or selector yet. If it succeeds, add the real navigation and operations one at a time.

Keep PHP output and exit status separate

Do not rely on a single captured output string. Browser diagnostics and Node logs belong on stderr; stdout should contain the result PHP expects, such as one JSON object. The following PHP 7.4+ example uses proc_open with an argument array, captures both streams, imposes a deadline, and terminates and reaps the child if it runs too long. Set $node and $script to absolute paths appropriate to the server.

<?php
$node = '/usr/bin/node';
$script = __DIR__ . '/capture.cjs';
$targetUrl = 'https://example.com';
$timeoutSeconds = 90;

$command = [$node, $script, $targetUrl];
$spec = [
    0 => ['pipe', 'r'],
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w'],
];
$env = array_merge($_ENV, [
    'HOME' => '/var/www',
]);
$process = proc_open($command, $spec, $pipes, __DIR__, $env);
if (!is_resource($process)) {
    throw new RuntimeException('Could not start the Node process');
}
fclose($pipes[0]);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);

$stdout = '';
$stderr = '';
$started = microtime(true);
$observedExitCode = null;
$timedOut = false;

while (true) {
    $stdout .= stream_get_contents($pipes[1]);
    $stderr .= stream_get_contents($pipes[2]);
    $status = proc_get_status($process);
    if (!$status['running']) {
        $observedExitCode = $status['exitcode'];
        break;
    }
    if (microtime(true) - $started > $timeoutSeconds) {
        $timedOut = true;
        proc_terminate($process);
        usleep(200000);
        $status = proc_get_status($process);
        if ($status['running']) {
            proc_terminate($process, 9);
        }
        break;
    }
    usleep(100000);
}

$stdout .= stream_get_contents($pipes[1]);
$stderr .= stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$closeCode = proc_close($process);
$exitCode = $observedExitCode !== null && $observedExitCode >= 0
    ? $observedExitCode
    : $closeCode;

if ($timedOut || $exitCode !== 0) {
    error_log(json_encode([
        'stage' => $timedOut ? 'php-timeout' : 'node-exit',
        'exit_code' => $exitCode,
        'stderr' => $stderr,
    ]));
    throw new RuntimeException($timedOut
        ? 'Puppeteer child process exceeded its deadline'
        : 'Puppeteer child process failed');
}
$result = json_decode($stdout, true, 512, JSON_THROW_ON_ERROR);

This example is a starting point, not a substitute for your application’s process-management policy. In production, avoid logging secrets from URLs or headers, validate the target URL, and set a deadline appropriate to the operation. If you use a persistent Node service rather than launching a process for every request, return structured error fields such as stage, message, stderr, and exit_code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fix missing Chrome or a browser cache mismatch

If the error says Chrome or the expected browser cannot be found, check whether Puppeteer’s browser installation ran, where its cache resides, and which account owns that cache. According to Puppeteer’s troubleshooting guide, since Puppeteer v19 the default downloaded-browser location is ~/.cache/puppeteer; the guide documents PUPPETEER_CACHE_DIR to relocate it.

  1. As the runtime account, inspect HOME and confirm the expected Puppeteer cache directory exists and is readable.
  2. If package-manager install scripts were blocked, run npx puppeteer browsers install during the build or deployment step that prepares the runtime.
  3. If the build and runtime use different accounts or machines, configure a shared cache directory with suitable read and execute permissions, or install the browser for the actual service account.
  4. For cached CI or hosted builds, ensure the browser cache persists from build to runtime. A cache present only in a temporary build environment will not help the deployed process.

Do not assume that installing Puppeteer in one account makes its browser available to PHP-FPM or a worker running as another account.

Check executable paths, versions, and launch failures

If you set Puppeteer’s executablePath, the path must point to a browser executable inside the machine or container where Node actually runs. Verify the path as the service user, check that it is executable, and confirm its dependent system libraries are installed. Puppeteer’s API reference warns that it is only guaranteed to work with the bundled browser; pin and test the Puppeteer/browser pair if you choose a system browser instead of the bundled one.

Expose Chromium’s actual stderr

For Failed to launch the browser process, enable Puppeteer’s dumpio: true launch option and inspect the browser’s stderr and exit status. The generic launch error often hides a more useful line about missing libraries, an invalid executable path, sandbox permissions, or insufficient privileges. Fix the underlying cause shown in that output rather than treating the generic message as a diagnosis.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use sandbox workarounds cautiously

Puppeteer’s troubleshooting guide discusses --no-sandbox in its GitLab CI example, alongside installation of required system packages. That flag is an environment-specific workaround, not a universal repair: do not add it automatically to every PHP launch. First identify whether the issue is genuinely sandbox permissions, and follow the security requirements of the host or container.

Give read-only containers writable browser directories

Chromium may need to write profile, configuration, and cache data before Puppeteer can connect. A read-only container can therefore fail during launch. Provide writable XDG configuration and cache locations, and an explicit writable userDataDir; ensure the runtime user owns them. Puppeteer’s launch API documents userDataDir as the browser profile directory.

Treat Alpine as a compatibility case

Puppeteer’s troubleshooting guide states that “Chrome does not support Alpine out of the box.” Match the Chromium package to a Puppeteer version that supports it and install the needed packages. The guide records timeout problems with the then-current Chromium in Alpine 3.20 and says Alpine 3.19 resolved that issue at the time of writing; that historical note is not a guarantee about current Alpine or Chromium releases. Verify the specific versions you deploy instead of copying an old version pairing blindly.

Fix PHP-FPM and other process-boundary failures

If PHP gets an empty response, first establish whether Node started and whether it exited cleanly. Compare the environment seen by the working shell with the environment seen by PHP: Node’s absolute path, current directory, HOME, cache variables, permissions, and the service account are common differences. PHP-FPM often has a restricted PATH, so a command that resolves as node in a terminal may not resolve from PHP.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Use an absolute Node executable path and an explicit working directory.
  • Pass required environment variables deliberately; do not assume the web server inherits your login shell configuration.
  • Capture stdout, stderr, and the process exit code independently. Keep stdout clean if PHP expects JSON.
  • Return or log a stage such as bridge-start, browser-launch, or navigation so an empty result does not erase where the failure occurred.
  • Set a bounded PHP wait and terminate and reap a child that exceeds it. In Node, close pages and the browser in a finally path so repeated calls do not accumulate orphaned Chromium processes.

If the work is queued or asynchronous, keep the worker alive until the Puppeteer promise settles. Puppeteer’s troubleshooting guide notes that cloud runtimes can suspend CPU after a response is sent; it documents this behavior for Cloud Run.

Debug timeouts and page-operation errors only after launch works

A navigation timeout is a different failure from a browser-start timeout. Puppeteer’s launch timeout governs the browser-start deadline; page navigation has its own timeout and wait strategy. Raising a launch timeout will not repair an unreachable URL, a page that never reaches the chosen load condition, or a selector that never appears.

Once the minimal launch test passes, log the failing operation and inspect the URL (redacting secrets), navigation timeout, HTTP or security error, selector, and target frame. A page or element may have been replaced before the operation ran. Add one operation at a time—navigation, screenshot or PDF, then selectors—to isolate which one first fails.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Choose a PHP-to-Puppeteer architecture that fits the workload

There is no single process arrangement that suits every deployment. Choose based on startup time, operational simplicity, permissions, cleanup, and how clearly failures reach the caller.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Choice What to weigh
Process per PHP request Straightforward isolation, but each request must start Node and Chromium; enforce deadlines and reliably clean up child processes.
Persistent Node service Avoids starting a new Node process for every PHP request, but requires service supervision, health handling, and clear request-to-error propagation.
Synchronous wait versus queue A synchronous response is simple when the operation fits the caller’s deadline. A queue is more suitable when work may outlive the web request, provided the worker remains active until Puppeteer finishes.
Bundled browser versus system executable The bundled browser is Puppeteer’s guaranteed pairing. A system executable may suit a managed environment, but requires explicit path, dependency, and version testing.
Shared versus isolated profile A shared cache can reduce repeated setup when permissions and persistence are correct. An explicit per-job writable profile can avoid profile collisions; clean it up after use.
Same host versus container A container can make runtime dependencies more reproducible, but must include compatible browser libraries, executable permissions, writable profile/cache paths, and deliberate sandbox configuration.

Or skip the browser setup

If you need screenshots from PHP without maintaining a Node-and-Chromium runtime, ScreenshotNeo provides a screenshot API and MCP server. Its HTTP API accepts a URL in one GET request and returns an image or PDF. For example, make the request from PHP with cURL:

<?php
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => 'https://api.screenshotneo.com/v1/shot?access_key=YOUR_API_KEY&url=' . rawurlencode('https://example.com'),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 90,
]);
$image = curl_exec($ch);
if ($image === false) {
    throw new RuntimeException(curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status < 200 || $status >= 300) {
    throw new RuntimeException('Screenshot request failed with HTTP ' . $status);
}
file_put_contents('shot.webp', $image);
?>

See the ScreenshotNeo documentation for request options and response details. It removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, and cache hits are not billed. Its MCP server exposes screenshot tools to AI agents, and the Free plan includes 1,000 screenshots a month without a card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo free to try 1,000 screenshots a month with no card.

Frequently asked questions

Is Puppeteer a PHP library?

No. Puppeteer is a Node.js library. PHP applications commonly call it by starting a Node process or by sending a request to a Node service.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How can I tell if the browser launched before a screenshot failed?

Use a minimal launch-and-close script first, then add navigation and capture. If that minimal script succeeds under the PHP service account, the failure lies in a later page operation rather than basic browser startup.

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.