ScreenshotNeo

BlogHow-to

PHP Browsershot screenshot timeout: common fixes

Find which Browsershot timeout fired, then fix the URL, readiness condition, runtime, or specific timeout setting causing it.

By the ScreenshotNeo team4 October 20269 min read

When a PHP Browsershot screenshot times out, first identify which operation timed out: the PHP-side process, Puppeteer navigation, a browser protocol operation, or a page readiness wait. Then verify that Chromium can reach the exact target URL from its own runtime environment, choose a readiness condition that fits the page, check the installed Browsershot and Puppeteer versions, and adjust only the matching timeout.

Increasing every timeout can make a failing job wait longer without fixing it. A longer limit helps only when the URL is reachable, dependencies are compatible, and the operation can eventually complete.

1. Identify the timeout layer

Save the complete exception, stack trace, and command output before changing configuration. The wording often tells you where to investigate. A message such as Navigation timeout of 30000 ms exceeded points to navigation or its readiness condition; it does not by itself prove that PHP’s process timeout or the browser protocol timeout fired.

Layer What it limits First thing to check
PHP-side process How long Browsershot lets its browser script run before the process is stopped. Is the script still doing useful work, or is it waiting on an unreachable URL or condition?
Navigation Loading a URL and satisfying the selected navigation wait. Can Chromium reach the URL, and is the navigation wait suitable for this page?
Browser protocol Commands exchanged with the browser over its protocol connection. Is a protocol operation hanging, and does the installed Browsershot version support the setting?
Readiness wait A selector, JavaScript condition, fixed delay, or network-idle state becoming ready. Can the condition ever become true on this page?

Browsershot exposes separate timeout() and protocolTimeout() settings. Puppeteer also has a default navigation timeout API, and its Page.goto() call can return a response, null for some navigation cases, or throw when navigation fails. Read the error and the operation being run before deciding which limit applies. See the [Browsershot source](https://github.com/spatie/browsershot/blob/main/src/Browsershot.php), [Puppeteer navigation timeout API](https://pptr.dev/api/puppeteer.page.setdefaultnavigationtimeout), and [Puppeteer Page.goto API](https://pptr.dev/api/puppeteer.page.goto).

2. Check URL reachability from Chromium

A URL that opens in your desktop browser may not be reachable from the process that runs Chromium. This is especially easy to miss with localhost: inside a container or another machine, it refers to that runtime’s own loopback interface, not necessarily your laptop or the PHP server you expected.

  1. Run the check from the same container, VM, or server that launches Chromium.
  2. Confirm the hostname resolves there and the expected port is listening.
  3. Follow redirects and check whether the final destination requires authentication.
  4. Check TLS trust, proxy settings, firewall rules, and any network restrictions in that runtime.
  5. Confirm the target server can handle the screenshot request while Browsershot is waiting for it.

Browsershot Discussion #516 describes an individual localhost case with a 30-second navigation timeout. The discussion suggests increasing PHP_CLI_SERVER_WORKERS where the PHP built-in server must handle more than one request. Treat that as a case-specific possibility: first verify that your flow uses the built-in server and that its request handling is the bottleneck. It is not a universal Browsershot setting or fix. [Read the localhost discussion](https://github.com/spatie/browsershot/discussions/516).

3. Wait for the page’s real readiness signal

Choose a wait condition that matches what the page needs before a screenshot. Network idle is convenient for static pages, but a page with polling, analytics, streaming, or other persistent network activity may never become idle. A reliable element or application state is often a better completion signal.

Use a selector or application condition

<?php
use Spatie\Browsershot\Browsershot;

Browsershot::url('https://example.com/report')
    ->waitForSelector('[data-report-ready]')
    ->save('/tmp/report.png');

Replace the selector with an element that appears only when the content you need is ready. If the readiness is represented by application state rather than an element, use waitForFunction() with a condition that can become true. Avoid selectors or predicates that depend on content the page never renders.

Use network idle only when the page supports it

Browsershot offers strict and non-strict network-idle modes, commonly identified as networkidle0 and networkidle2. Strict idle can be defeated by any continuing request; the less strict mode tolerates some activity. Neither is a guarantee that application rendering is complete. Check the installed package’s options and select the mode that fits the site’s network behavior.

Use a delay as a last resort

A fixed wait can help when the page has no observable readiness signal, but it adds the full delay to every capture and may still be too short under load. Prefer a selector or state check where possible. Browsershot’s wait options and their exact API are version-sensitive; confirm them in the [current source](https://github.com/spatie/browsershot/blob/main/src/Browsershot.php) and your installed package.

4. Check installed versions and browser paths

Browsershot runs a Node.js browser script and needs a compatible Puppeteer and Chrome or Chromium executable in the environment where PHP executes it. A developer machine can have a different Node version, browser binary, or module path from the production container.

  1. Inspect the installed Browsershot version in the lockfile or with Composer’s package information.
  2. Confirm Node.js and Puppeteer are installed where the PHP worker runs, not just in an interactive shell.
  3. Check custom Node, Puppeteer module, and Chrome/Chromium paths if you configured them.
  4. Verify the executable exists and has permission to run as the PHP worker user.
  5. Compare the project’s installed API with the documentation or examples you are following.

The Browsershot changelog says version 5.0.0 requires Puppeteer 23.0 or higher and that protocol-timeout options were added in 4.2.0. These are version-specific facts, so check the version actually installed before copying configuration. [See the Browsershot changelog](https://github.com/spatie/browsershot/blob/main/CHANGELOG.md).

5. Set only the relevant timeout

Browsershot’s timeout($seconds) accepts seconds. Its browser script option is expressed in milliseconds, so the method converts the supplied value. The source currently defines a 60-second default process timeout, but defaults can change; inspect the source for your installed version. protocolTimeout() is a separate setting and should only be changed when the failing operation is a browser protocol command.

<?php
use Spatie\Browsershot\Browsershot;

Browsershot::url('https://example.com/report')
    ->timeout(90)          // seconds for the Browsershot browser script
    ->protocolTimeout(90_000) // protocol timeout, in milliseconds in supported versions
    ->waitForSelector('[data-report-ready]')
    ->save('/tmp/report.png');

Use the protocol setting only if your installed Browsershot release supports it and the error points to a protocol operation. Do not assume a longer process timeout also changes Puppeteer’s navigation timeout. Puppeteer’s Page.setDefaultNavigationTimeout() is a separate API; when you need that exact control, use the Puppeteer setup or Browsershot option supported by your installed version. Consult [Browsershot’s options](https://github.com/spatie/browsershot/blob/main/src/Browsershot.php) and [Puppeteer’s navigation timeout API](https://pptr.dev/api/puppeteer.page.setdefaultnavigationtimeout).

6. Complete runnable PHP examples

Capture a URL with a targeted selector wait

<?php
require __DIR__ . '/vendor/autoload.php';

use Spatie\Browsershot\Browsershot;

$url = 'https://example.com/report';
$output = __DIR__ . '/report.png';

Browsershot::url($url)
    ->waitForSelector('[data-report-ready]')
    ->timeout(90)
    ->save($output);

echo "Saved screenshot to {$output}\n";

Capture rendered HTML

<?php
require __DIR__ . '/vendor/autoload.php';

use Spatie\Browsershot\Browsershot;

$html = '<!doctype html><html><body><h1>Rendered report</h1></body></html>';
$output = __DIR__ . '/report.png';

Browsershot::html($html)
    ->timeout(60)
    ->save($output);

echo "Saved screenshot to {$output}\n";

These examples assume the project has installed Spatie Browsershot and its required Node/Puppeteer/browser runtime. The selector example also assumes the target page actually renders the supplied readiness selector. If those assumptions do not hold, fix the runtime or condition rather than raising the limits again.

7. Keep Chrome CLI timeouts separate

Chrome’s standalone headless command-line --timeout controls when the CLI captures content even if the page is still loading. It is not Browsershot’s PHP timeout() method or its protocol timeout. Apply Chrome CLI advice only when you are invoking Chrome’s CLI directly. [Chrome Headless command-line reference](https://developer.chrome.com/docs/automation-and-testing/headless-cli).

8. Troubleshooting checklist

Symptom Likely area What to do
Navigation timeout of 30000 ms exceeded Navigation or readiness wait Test the URL from Chromium’s runtime; inspect redirects and choose a selector or state wait if network idle never occurs.
Desktop browser works, screenshot worker times out Runtime networking or localhost Resolve the host and port from the worker/container; confirm the target server is reachable there.
The page keeps making requests Network-idle wait Use a meaningful selector or waitForFunction(), or the less strict idle mode if suitable.
Timeout appears after changing dependencies Version compatibility Check Browsershot, Puppeteer, Node, and browser versions together; verify paths and executable permissions.
Browser protocol operation times out Protocol timeout Check support in the installed Browsershot version and adjust protocolTimeout() only if the operation can succeed with more time.
PHP built-in server is involved and target is localhost Server request handling Inspect the request flow and worker capacity; consider PHP_CLI_SERVER_WORKERS only if it matches the reported case.
Advice mentions Chrome CLI --timeout Different execution path Use it only for direct Chrome headless CLI captures, not as a substitute for Browsershot settings.

9. Performance, reliability, and cost

  • Performance: A readiness condition that resolves as soon as the necessary content exists avoids waiting for unrelated requests. Large pages, slow third-party assets, and arbitrary fixed waits all add capture time.
  • Reliability: Test from the same runtime and user account as the production PHP worker. Make waits conditional on page state, and make the target server, browser executable, and dependency versions explicit in deployment configuration.
  • Timeout policy: Set a limit long enough for valid slow pages, while keeping a finite upper bound so unreachable destinations do not occupy workers indefinitely. A larger limit does not improve reachability or compatibility.
  • Cost: Self-hosting means maintaining the PHP, Node, Puppeteer, and browser runtime and accounting for worker time and infrastructure use. If you use a managed capture service, compare its billing rules and required setup against your workload.

10. Or skip the browser setup

If maintaining Node.js, Puppeteer, Chromium, and their runtime paths is the part slowing you down, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns an image or PDF; see the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/report -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/report"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/report' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the capture. Bot checks, blank pages, failed loads, and cache hits are never billed; response headers report the page verdict and billing status. Its MCP server lets Claude, Cursor, and other MCP clients take screenshots. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

FAQ

Does Browsershot’s timeout argument use seconds or milliseconds?

timeout() takes seconds and converts the value for the browser script. Do not pass milliseconds to it without checking the installed version’s API.

Will raising the timeout fix every navigation error?

No. It only helps an operation that can succeed but needs longer. It cannot make an unreachable URL, missing executable, incompatible dependency, or never-satisfied wait succeed.

Why does localhost work in my browser but not in the screenshot?

The browser launched by PHP may run in a different container or host. There, localhost points to that runtime. Test the exact address from the Chromium environment.

Should I always wait for network idle?

No. Pages with continuing requests may never reach idle. Wait for the element or application state that proves the content needed for the screenshot is ready.

Is Chrome’s --timeout the same as Browsershot’s timeout?

No. Chrome’s flag applies to direct headless CLI capture behavior. Browsershot has its own PHP and browser settings.