ScreenshotNeo

BlogHow-to

How to Fix Screenshot Timeouts in syn-nodejs-puppeteer 13.1

Diagnose screenshot timeouts in syn-nodejs-puppeteer 13.1 by separating canary, navigation, and CDP limits, then fix the failing layer.

By the ScreenshotNeo team1 October 20267 min read

Screenshot timeouts in syn-nodejs-puppeteer-13.1 are symptoms, not a single error. First identify whether the timeout comes from the CloudWatch Synthetics canary run, page navigation/readiness, or the Chrome DevTools Protocol screenshot command. Puppeteer’s documented page.screenshot() options do not include a per-call timeout field, so adding page.screenshot({ timeout: ... }) is not a documented fix.

A reliable investigation is:

  1. Save the complete exception, stack trace, canary logs, and runtime version.
  2. Classify the failing layer.
  3. Make the whole canary timeout large enough for startup, navigation, readiness checks, and capture.
  4. Reduce concurrency and simplify the reproduction.
  5. Test any protocolTimeout change as a diagnostic hypothesis, not a guaranteed repair.
  6. Reproduce with the exact AWS-managed runtime before concluding that Puppeteer or AWS is at fault.

What syn-nodejs-puppeteer 13.1 contains

AWS documents this runtime as Lambda Node.js 22.x with Puppeteer-core 24.25.0, Chromium 142.0.7444.175, and Firefox 145.x. Version 13.1 also introduced a Synthetics runtime namespace migration. Use the 13.1 namespace and matching type definitions in your canary. See the AWS runtime documentation.

Identify which timeout you have

Observed symptom Likely layer What to inspect
The canary run ends before the script finishes CloudWatch run timeout Canary timeout, frequency-derived default, cold start and instrumentation time
Navigation timeout of ... ms exceeded Navigation goto() timeout, URL response, redirects, page readiness condition
ProtocolError: Page.captureScreenshot timed out CDP screenshot operation Browser state, page complexity, concurrent tabs, protocol timeout
The page is loaded but the capture hangs Readiness or browser state Network-idle waits, image-heavy pages, open connections, concurrent work

Do not collapse these into one “screenshot timeout.” A canary timeout limits the complete run. A navigation timeout applies to loading. The screenshot call is a separate browser operation.

1. Check and raise the canary run budget

CloudWatch allows a configured timeout for the entire canary run. If you omit it, CloudWatch chooses a value based on the canary frequency. AWS recommends configuring at least 15 seconds so Lambda cold starts and canary instrumentation have time to start. Treat 15 seconds as a floor, not a promise that your workflow will complete within that time.

Checklist

  • Measure startup, navigation, page readiness, screenshot, assertions, and cleanup separately in logs.
  • Include cold starts and Synthetics instrumentation startup in the budget.
  • Leave headroom for slow responses and retries.
  • Confirm the timeout is the canary setting, not an assumed Puppeteer screenshot option.

2. Use supported screenshot options

The Puppeteer ScreenshotOptions API documents options such as fullPage, clip, path, encoding, quality, and image type. It does not document a per-call timeout property. A call such as this is therefore not a supported timeout control:

await page.screenshot({ path: '/tmp/shot.png', timeout: 60000 });

Use the canary timeout for the complete workflow and control navigation/readiness explicitly. Keep screenshot options limited to documented capture choices.

3. Build a minimal diagnostic canary

Start with one page, one tab, a simple URL, and a short sequence. This tells you whether the failure requires the target page, concurrency, or a wait condition.

const synthetics = require('@aws/synthetics-puppeteer');

const URL = 'https://example.com';

const handler = async () => {
  const browser = await synthetics.launch();
  const page = await browser.newPage();

  page.setDefaultNavigationTimeout(30000);
  page.setDefaultTimeout(30000);

  console.time('goto');
  await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
  console.timeEnd('goto');

  await page.screenshot({
    path: '/tmp/example.png',
    type: 'png',
    fullPage: true
  });

  await browser.close();
};

exports.handler = async () => {
  await handler();
};

Replace example.com with the failing target only after this minimal path works. Match the runtime’s namespace and type definitions for 13.1.

4. Test navigation and readiness separately

networkidle2 waits until there are no more than two active network connections for a period. Analytics, streaming requests, advertisements, and long polling can keep that condition from becoming useful. Compare it with domcontentloaded and an explicit selector that represents readiness.

await page.goto(url, {
  waitUntil: 'domcontentloaded',
  timeout: 30000
});

await page.waitForSelector('[data-page-ready]', {
  visible: true,
  timeout: 15000
});

await page.screenshot({ path: '/tmp/ready.png', fullPage: true });

Use a selector that is stable for your application. If no readiness marker exists, use a bounded delay only as a diagnostic, then replace it with a deterministic condition where possible.

5. Treat protocolTimeout as a hypothesis

Puppeteer exposes a browser connection protocol timeout that can affect protocol commands. One issue report says changing protocolTimeout stopped an operation from remaining stuck, while another reports Page.captureScreenshot timing out even with a three-minute protocol timeout. Increasing it may allow a protocol operation more time, but it does not prove that the capture will recover or that the original timeout was simply too short.

const puppeteer = require('puppeteer-core');

const browser = await puppeteer.launch({
  protocolTimeout: 180000
});

const page = await browser.newPage();
await page.goto('https://example.com', {
  waitUntil: 'domcontentloaded',
  timeout: 30000
});
await page.screenshot({ path: '/tmp/shot.png', fullPage: true });
await browser.close();

In an AWS Synthetics canary, use the launch mechanism and browser supplied by the managed runtime. Do not assume that a local puppeteer.launch() example is interchangeable with the Synthetics launcher. If you change protocol timeout, record the value and compare the result with a baseline.

6. Reduce concurrency and isolate page conditions

A recent open issue reports a reproduction involving Docker, multiple tabs, an image target, and waitUntil: 'networkidle2'. That report used Puppeteer 24.38.0 and Node 24.4.1, so it is not confirmation of a defect in AWS’s 13.1 bundle. It does identify useful variables for a minimal reproduction:

  • Run one tab instead of several.
  • Capture a normal HTML page before an image-heavy target.
  • Compare domcontentloaded with networkidle2.
  • Run serially before introducing parallel captures.
  • Keep the same browser, runtime, viewport, and target type as the failing canary.

7. Add timing and failure logs

function mark(label) {
  console.log(JSON.stringify({
    label,
    at: new Date().toISOString()
  }));
}

try {
  mark('before-goto');
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
  mark('after-goto');

  await page.waitForSelector('[data-page-ready]', { timeout: 15000 });
  mark('after-ready');

  await page.screenshot({ path: '/tmp/shot.png', fullPage: true });
  mark('after-screenshot');
} catch (error) {
  console.error(JSON.stringify({
    name: error.name,
    message: error.message,
    stack: error.stack
  }));
  throw error;
}

The last emitted marker identifies the failing stage. Preserve the complete exception rather than only the final message.

Troubleshooting common failures

Error or symptom Cause to investigate Fix or experiment
Canary times out with no screenshot error Whole-run budget is too short Increase the canary timeout; include startup, instrumentation, navigation, readiness, capture, and cleanup.
Navigation timeout ... exceeded Slow response, redirect chain, or unsuitable wait condition Log redirects and response timing; test domcontentloaded; set a bounded navigation timeout.
ProtocolError: Page.captureScreenshot timed out CDP capture stalled or browser is under load Reproduce with one tab and a simple page; compare protocol timeout values; do not treat the setting as guaranteed.
Timeout after using networkidle2 Persistent requests prevent useful network-idle completion Use domcontentloaded plus an application readiness selector.
Works locally but fails in Synthetics Different browser, runtime, instrumentation, memory, or startup cost Match syn-nodejs-puppeteer 13.1 and reproduce inside the managed environment.
Fails only with several tabs Concurrency or browser resource pressure Serialize captures, close pages promptly, and add tabs one at a time.

Performance and reliability practices

  • Prefer one capture per page while diagnosing.
  • Use a readiness selector instead of an unbounded network-idle assumption.
  • Bound navigation and selector waits so the canary can fail with a useful error.
  • Capture only the required region when full-page output is unnecessary.
  • Close pages and browsers in cleanup paths.
  • Keep a reproducible record of runtime version, browser, URL type, tab count, wait condition, and protocol timeout.
  • Retry only at the workflow level when the operation is safe to repeat; retries consume canary time and can hide a deterministic failure.

Cost and operational notes

Longer canary timeouts and retries increase execution time and can delay detection. A larger protocol timeout may leave a stuck browser operation waiting longer without producing an image. Choose values from observed stage timings and leave headroom for cold starts rather than setting every timeout to an arbitrary large number.

Or skip the browser setup

If your goal is a reliable image rather than maintaining a Lambda browser, ScreenshotNeo provides a GET screenshot API and MCP server. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo API documentation for the full option set.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
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}`);

ScreenshotNeo includes full-page and element capture, device and viewport controls, retina scale, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agents, geolocation, caching, signed links, asynchronous jobs, bulk capture, PDF output, and a usage API. It offers 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can I set a timeout directly on page.screenshot()?

Not through the documented Puppeteer ScreenshotOptions API. Configure the canary run budget, navigation/readiness waits, and, for diagnosis, the browser protocol timeout.

Is 15 seconds always enough?

No. AWS recommends at least 15 seconds for configured canary timeouts to cover startup overhead. Your page and workflow may require more.

Will increasing protocolTimeout fix capture timeouts?

It may change the behavior of a protocol operation, but issue evidence includes failures even with a three-minute value. Treat it as an experiment, not a guaranteed fix.

Should I always use networkidle2?

No. Persistent analytics, ads, streaming, or long-polling connections can make it a poor readiness signal. Compare it with domcontentloaded and an explicit selector.

What is the first reproduction to share with AWS?

Provide the exact runtime, browser, URL type, tab count, navigation condition, complete stack trace, timing markers, and a minimal one-tab script that still fails.