Skip to content

fix(browser): raise protocolTimeout to 10min for heavy pages#2010

Open
triuzzi wants to merge 1 commit into
ChromeDevTools:mainfrom
triuzzi:feat/protocol-timeout-upstream
Open

fix(browser): raise protocolTimeout to 10min for heavy pages#2010
triuzzi wants to merge 1 commit into
ChromeDevTools:mainfrom
triuzzi:feat/protocol-timeout-upstream

Conversation

@triuzzi

@triuzzi triuzzi commented May 7, 2026

Copy link
Copy Markdown

Summary

Heavy pages (e.g. dev bundles >100MB) cannot ack Network.enable and other auto-attached CDP domain calls within puppeteer's default 180s. Once the timeout fires, puppeteer marks the connection dead and every subsequent call throws Network.enable timed out — only daemon restart recovers.

Set protocolTimeout: 600000 on both puppeteer.connect() and puppeteer.launch(). Env-overridable via CHROME_DEVTOOLS_PROTOCOL_TIMEOUT_MS for power users.

Repro

Hit during real-world testing against a Brightcove Studio dev bundle (~160MB JS):

$ chrome-devtools-mcp start --browserUrl http://127.0.0.1:9222
$ chrome-devtools-mcp list_pages
[{"type":"text","text":"Network.enable timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed."}]

After the failure, every subsequent call returned the same timeout. The daemon process was alive (process.kill(pid, 0) returned true), so client retries reused the wedged connection until manually killed.

Why 10min

Puppeteer's 180s default is calibrated for small pages and CI runners. For real-world dev work (HMR-enabled webpack bundles, large React apps), 10min is comfortable headroom while still bounded enough to surface genuine deadlocks. Env-overridable so power users with even-larger workloads aren't blocked.

Testing

  • npm run build clean
  • Verify on a real heavy-page session that Network.enable timed out no longer surfaces

Note

A previous PR (#2009) was opened from a downstream fork and accidentally pulled in 9 fork-specific commits — closed and re-submitted as this clean single-commit PR branched from main.

🤖 Generated with Claude Code

@google-cla

google-cla Bot commented May 7, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@triuzzi

triuzzi commented May 7, 2026

Copy link
Copy Markdown
Author

@googlebot I signed it!

@triuzzi triuzzi force-pushed the feat/protocol-timeout-upstream branch from a52a18a to 38a09eb Compare May 7, 2026 14:09
Heavy pages (e.g. dev bundles >100MB) cannot ack `Network.enable` and
other auto-attached CDP domain calls within puppeteer's default 180s.
Once the timeout fires, puppeteer marks the connection dead and every
subsequent call throws `Network.enable timed out` — only daemon
restart recovers.

Set `protocolTimeout: 600000` on both `puppeteer.connect()` and
`puppeteer.launch()`. Env-overridable via
`CHROME_DEVTOOLS_PROTOCOL_TIMEOUT_MS` for power users.

Hit in real workloads against a Brightcove Studio dev bundle (~160MB
JS) where the default timeout fired before `list_pages` could complete,
leaving the daemon permanently wedged.
@triuzzi triuzzi force-pushed the feat/protocol-timeout-upstream branch from 38a09eb to 5348bf7 Compare May 7, 2026 14:10

@OrKoN OrKoN left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please provide a test site you see this with?

@triuzzi

triuzzi commented Jun 22, 2026

Copy link
Copy Markdown
Author

Ciao!

The original issue arose with a private frontend project, a ~70 MB Studio dev bundle I sadly cannot share, but the cause is engine- and site-agnostic, so here's a minimal deterministic repro.

Root cause. On connect()/auto-attach, puppeteer sends the init batch (Network.enable, Page.enable, Runtime.enable, …) per page target. Those CDP responses are serviced on that target's renderer main thread. If the main thread is pegged longer than protocolTimeout, the batch never acks and puppeteer rejects with Network.enable timed out. A 160 MB bundle pegs it via parse/compile; any long synchronous task does the same.

Test page — pegs the renderer main thread for 200s (> the 180s default):

<!doctype html><meta charset=utf-8><title>main-thread blocker</title>
<h1>pegging the renderer main thread for 200s</h1>
<script>const end=Date.now()+200000;while(Date.now()<end){}</script>

Faithful repro via chrome-devtools-mcp (~180s): launch Chrome with --remote-debugging-port=9222, open the page above, then point the server at it (--browserUrl http://127.0.0.1:9222) and call list_pages. After 180s it returns Network.enable timed out.

Mechanism proof (~3s) — identical connect → auto-attach → list_pages flow, with protocolTimeout lowered so you don't wait 180s:

import puppeteer from 'puppeteer-core';

const launchTarget = process.env.BROWSER_PATH
  ? {executablePath: process.env.BROWSER_PATH}
  : {channel: 'chrome'};

const BLOCK_MS = parseInt(process.env.BLOCK_MS ?? '30000', 10);
const CLIENT_PROTOCOL_TIMEOUT_MS = parseInt(
  process.env.CLIENT_PROTOCOL_TIMEOUT_MS ?? '3000',
  10,
);

const blockingHtml = `<!doctype html><meta charset="utf-8">
<title>main-thread blocker</title>
<script>
  // Stand-in for "parse/compile a 160MB JS bundle": occupy the main
  // thread synchronously so the renderer cannot service CDP messages.
  const end = Date.now() + ${BLOCK_MS};
  while (Date.now() < end) {}
</script>`;
const blockingUrl = 'data:text/html,' + encodeURIComponent(blockingHtml);
const fmt = e => (e?.message ?? String(e)).split('\n')[0];

const server = await puppeteer.launch({
  ...launchTarget,
  headless: true,
  args: ['--no-first-run', '--no-default-browser-check'],
});
try {
  const page = await server.newPage();
  page.goto(blockingUrl).catch(() => {}); // hangs — main thread is pegged
  await new Promise(r => setTimeout(r, 1500));

  const t0 = Date.now();
  try {
    // What chrome-devtools-mcp does on --browserUrl / list_pages:
    const client = await puppeteer.connect({
      browserWSEndpoint: server.wsEndpoint(),
      protocolTimeout: CLIENT_PROTOCOL_TIMEOUT_MS,
    });
    for (const p of await client.pages()) await p.title();
    console.log('no timeout — list_pages succeeded');
  } catch (e) {
    console.log(`REPRODUCED after ${Date.now() - t0}ms: ${fmt(e)}`);
  }
} finally {
  await server.close().catch(() => server.process()?.kill('SIGKILL'));
}

Verified on Chrome 149 and Brave, puppeteer-core 25.1.0:

protocolTimeout=3000,  block=30000 → REPRODUCED after 3004ms: Network.enable timed out. Increase the 'protocolTimeout' setting …
protocolTimeout=10000, block=4000  → no timeout — list_pages succeeded

The second line is exactly what this PR buys: once protocolTimeout exceeds the main-thread stall, the enable batch acks and the session recovers instead of wedging the connection.


Thank you in advance!

@triuzzi triuzzi requested a review from OrKoN June 22, 2026 15:24
@OrKoN

OrKoN commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Thanks! If main thread is blocked indefinitely, increasing time out would not help. So do you have an actual website that takes 180 seconds to load blocking the main thread for that long? Is it a public app we can test with? How do users use it if it takes 3 minutes to load?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants