Repository navigation
Description
Version
1.65.0
Steps to reproduce
Minimal repro repo: https://github.com/caihongdepiaoyi/pw-busy-renderer-repro
- Clone the repo and
npm install(deps:playwright-core@1.65.0only) node repro-busy-renderer.mjs— launches Chromium with--remote-debugging-port=9333, opens a page whose renderer runs an endless synchronous loop (for(;;)) after 100 ms, then attempts a secondconnectOverCDP()withtimeout: 10000→TimeoutErrorafter ~10000 msnode repro-crashed-page-control.mjs(control case) — same setup, but the pre-existing page's renderer is crashed viaPage.crashinstead of busy →CONNECTED in ~10 ms(proves the fix(chromium): do not hang on pages without a renderer when connecting over CDP #42936 fix is active for its scope)
Core of the busy repro (full files in the repo):
import { chromium } from 'playwright-core';
const PORT = 9333;
const browser = await chromium.launch({ args: [`--remote-debugging-port=${PORT}`] });
const user = await chromium.connectOverCDP(`http://127.0.0.1:${PORT}`);
const page = await user.contexts()[0].newPage();
await page.goto('data:text/html,<script>setTimeout(()=>{for(;;){}},100)</script>');
await new Promise(r => setTimeout(r, 600));
const t0 = Date.now();
try {
const second = await chromium.connectOverCDP(`http://127.0.0.1:${PORT}`, { timeout: 10000 });
console.log('CONNECTED in', Date.now() - t0, 'ms');
await second.close();
} catch (e) {
console.log(e.constructor.name, 'after', Date.now() - t0, 'ms');
}
await browser.close();Expected behavior
connectOverCDP should not be held hostage by a page that is neither crashed nor discarded — merely busy. A page that answers nothing within some bounded window during connect could either:
- be left to finish initializing in the background (it would still be reported via the regular
pageevent once it responds), or - cause connect to fail fast with a clear error listing the offending target.
Either way, one busy tab out of N should not block attaching to the browser until the overall timeout expires.
Actual behavior
On playwright-core 1.65.0 (verified that the #42936 fix is included and works for its scope), connectOverCDP() still hangs indefinitely when a pre-existing page's renderer is alive but its main thread is blocked by a long synchronous JavaScript task.
CRBrowser.connect awaits _waitForAllPagesToBeInitialized() — an unbounded Promise.all over every pre-existing page. A busy renderer never crashes, so Inspector.targetCrashed never fires and Inspector.enable itself is never answered, so one busy tab stalls the whole connect until its long task finishes (verified: a connection started mid-task resolves after ≈ the task's remaining runtime).
A/B on the same Chromium build, same port, same playwright 1.65.0:
| pre-existing page state | second connectOverCDP |
notes |
|---|---|---|
renderer crashed via Page.crash (not awaited) |
CONNECTED in 10 ms | #42936 fix is active ✔️ |
renderer busy in for(;;) |
TimeoutError after 10003 ms | still hangs ❌ |
During the hang, GET /json/version answers in 3 ms and a raw page-level WebSocket to a healthy page answers in a few ms — the DevTools HTTP/WS layers are fine; only the commands sent to the busy page's renderer (Page.enable, Runtime.enable, Inspector.enable, …) never return, and the overall timeout eventually fires: TimeoutError after ~10000 ms.
Additional context
Why this matters. Real-world triggers include a heavy synchronous render, a long GC pause, a page stuck in a long loop, or simply a heavy page being loaded — no crash is involved, and when the task eventually finishes, the page is perfectly healthy again. Clients that attach to long-lived browsers with many tabs (one blocked tab out of N) fail to connect at all, and retrying is ineffective because every retry runs into the same unbounded wait. #42730 and #42882 reported the same symptom and both were closed as duplicates (Sep 21 / Sep 28) before #42936 merged (Sep 30); #42936's mechanism (targetCrashed reporting) structurally cannot cover the alive-but-busy case.
Why this is not a duplicate of #42730 / #42882. Both report this exact symptom — #42882's repro is literally the same busy-renderer script as the one above — and both are closed: #42730 as a duplicate on Sep 21, #42882 (along with its unmerged fix PR #42883, a bounded-grace-period approach that would have covered this case) on Sep 28 — both before #42936 merged on Sep 30. #42936 is the only fix that ever shipped, and it covers crashed/discarded targets only. The A/B above on 1.65.0 (latest stable) shows the alive-but-busy case still hangs, and no open issue tracks it — hence this follow-up. Happy for it to be folded into a reopened #42730/#42882 or a tracking issue instead, whichever maintainers prefer.
Root cause. Unchanged since #41093/#42882: in CRBrowser.connect() (persistent-context branch taken by connectOverCDP), the final step is await browser._waitForAllPagesToBeInitialized() with no per-page bound. One possible direction is the one explored in #42883 (give a page a bounded grace period during connect, then let it initialize in the background and re-apply any context-level updates it missed); happy to help resurrect that approach in whatever shape maintainers prefer.
Environment
System:
OS: macOS 26.2
CPU: (14) arm64 Apple M4 Pro
Memory: 762.59 MB / 24.00 GB
Binaries:
Node: 24.18.0
npm: 11.16.0
npmPackages:
playwright-core: ^1.65.0 => 1.65.0
Activity
microsoft-playwright-automation commentedon Nov 11, 2026
microsoft-playwright-automation commentedon Nov 11, 2026
Hi, I'm the Playwright bot. I reproduced this and checked an early-return change.
Reproduced on current main (
1.66.0-next,3b438229c6a2ccdbe2d3572755935892203d83c3), Chromium 158.0.8127.18 on macOS, headed and headless. A busy renderer still blocks attachment;noDefaultsdoes not help. This case is not fixed by #42936.I have not proposed a fix. Returning early changes which existing pages are available after connection, and simply bounding the wait loses context updates: in an experiment, an init script added after connection never reached the delayed page, even after it recovered and navigated again. This needs an explicit partial-readiness API policy, rather than a timeout around the existing wait. I recommend an opt-in mode that preserves today's complete initial page list by default and reconciles context updates before exposing delayed pages. The compatibility concern was also recorded in the review of #41128.
Reproduction and results
This fixture-based test fails at
connectOverCDP, after the WebSocket connects. The console event confirms that the loop has started, without a timing guess.noDefaultsnoDefaults/json/versionduring finite-loop connection failuresTimeoutErrorAdditional checks on unchanged main:
The temporary early-return experiment raced initialization against 500 ms. It returned only the healthy page. After
context.addInitScript('window.connectionMarker = "installed"'), the busy page eventually appeared, but the marker remainedundefinedafter navigation. Without that experiment, the marker was"installed". The experimental source change was reverted.Commands used:
npm run build;npm run ctest -- tests/library/chromium/connect-over-cdp-busy.spec.ts --workers=1 --retries=0 --reporter=line(also--headed); the context-init-script case separately on main and the experiment; andconnect-over-cdp.spec.ts --grep 'CD-00[234]|CD-017', withCD-004rerun underDEBUG=pw:api.Only current main was tested, not the reported 1.65.0 package. Firefox and WebKit do not support
connectOverCDP, so they are not applicable. No public API or default behavior was changed.Triaged on macOS by the Playwright bot - agent run
vaidehi-snk commentedon Nov 11, 2026
I’d like to work on this issue if community contributions are welcome. The busy-renderer repro is clear, and I’ll make sure any fix keeps healthy pages connectable while preserving the existing page initialization and context-update behavior. Could a maintainer confirm the approach and assign the issue to me before I start?