You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug] playwright-cli: one discarded (Memory Saver) tab makes every command time out — tab header rendering awaits page.title() without timeout #41714
Environment: Windows 11, Chrome 150.0.7871.101, attaching to a long-running real Chrome profile.
Run Chrome with --remote-debugging-port=9222 --user-data-dir=<dedicated profile> and use it normally for a few days with several tabs open (Stripe/Azure/Vercel dashboards in my case).
Chrome's Memory Saver discards/freezes one of the idle background tabs. (Likely forcible on demand via chrome://discards → "Urgent discard", though my occurrence was organic.)
Run any command: playwright-cli tab-list, eval "location.href", snapshot, …
Expected behavior
Commands work, with the discarded tab either listed as-is (title from target info) or marked unresponsive.
Actual behavior
Every command fails after 30s with:
### Error
TimeoutError: Timeout 30000ms exceeded.
The daemon is fully wedged — even commands that don't touch the bad tab (e.g. eval on the current tab) fail, because every command's response renders the "Open tabs" / "Page" header sections.
Diagnosis
The discarded tab's renderer never answers renderer-bound CDP calls. Since #41128 (fix for #41093), connectOverCDP no longer hangs at connect time on such a tab — it reports the page "as-is". The result is a half-initialized Page whose url() returns "" and whose title() / evaluate() never resolve (they don't recover even on fresh connections, or after the renderer is revived via Target.activateTarget / Page.navigate; only closing the tab clears it).
The tools backend then hangs on it for every command: Tab.headerSnapshot() does
awaitthis._raceAgainstModalStates(async()=>{title=awaitthis.page.title();// no timeout, no catch});
(packages/playwright-core/src/tools/backend/tab.ts, visible at lib/coreBundle.js:63606 in the published bundle) and response rendering awaits headerSnapshot() for all tabs on every command. One zombie tab ⇒ every command times out.
Probing each page with Promise.race([page.title(), timeout]) over a raw connectOverCDP connection pinpoints the culprit: five healthy tabs answer in ms; the discarded one (empty url()) hangs on both title() and evaluate('1+1'), while browser-side CDP on the same target (Target.getTargetInfo) still answers fine and even knows the real title/URL.
Suggested fix
Guard the per-tab header with a small timeout and/or .catch() (like collectConsoleMessages already does), falling back to TargetInfo.title/url from the browser session, and render the tab as e.g. (unresponsive). Related report of the same underlying condition in another CDP-attaching agent tool: vercel-labs/agent-browser#1036.
Workaround
Close and recreate the discarded tab via a browser-level CDP session (Target.closeTarget + Target.createTarget) — the CLI heals instantly.
Hi, I'm the Playwright bot and I took a first look at this.
Confirmed by reading the source on tip-of-tree (sha 9fb3602) — your diagnosis is spot on. This is a real bug in the @playwright/cli tools backend (which lives in this repo, so it's ours to fix). A single tab whose renderer never answers renderer-bound CDP calls wedges every command.
The two pieces line up exactly as you described:
Tab.headerSnapshot() awaits page.title() with no timeout and no .catch() — unlike the neighbouring collectConsoleMessages(), which already guards with .catch(() => []):
// packages/playwright-core/src/tools/backend/tab.tsawaitthis._raceAgainstModalStates(async()=>{title=awaitthis.page.title();// no timeout, no catch});
Response rendering fans headerSnapshot() out across all tabs on every command:
So one discarded tab whose title() never resolves blocks the Promise.all, and every command — even ones that don't touch the bad tab — dies at the 30s timeout. That matches your report precisely.
What I checked
Read packages/playwright-core/src/tools/backend/tab.ts, response.ts, and tabs.ts on tip-of-tree.
Verified headerSnapshot() → page.title() has no timeout/catch, while collectConsoleMessages() two methods up does use .catch().
Verified both render callers (response.ts:274, tabs.ts:62) Promise.all over every tab, confirming that one hung tab stalls all output.
I didn't stand up a synthetic frozen/discarded Chrome tab to reproduce the CDP hang end-to-end — that condition is hard to force deterministically — but the code paths you point at are exactly as described, so the failure mode is unambiguous. Your suggested fix (bound the per-tab header with a small timeout/.catch() and fall back to TargetInfo.title/url, rendering the tab as unresponsive) is the right shape.
Activity
github-actions commentedon Jul 9, 2026
Hi, I'm the Playwright bot and I took a first look at this.
Confirmed by reading the source on tip-of-tree (sha
9fb3602) — your diagnosis is spot on. This is a real bug in the@playwright/clitools backend (which lives in this repo, so it's ours to fix). A single tab whose renderer never answers renderer-bound CDP calls wedges every command.The two pieces line up exactly as you described:
Tab.headerSnapshot()awaitspage.title()with no timeout and no.catch()— unlike the neighbouringcollectConsoleMessages(), which already guards with.catch(() => []):headerSnapshot()out across all tabs on every command:backend/tabs.ts:62)So one discarded tab whose
title()never resolves blocks thePromise.all, and every command — even ones that don't touch the bad tab — dies at the 30s timeout. That matches your report precisely.What I checked
packages/playwright-core/src/tools/backend/tab.ts,response.ts, andtabs.tson tip-of-tree.headerSnapshot()→page.title()has no timeout/catch, whilecollectConsoleMessages()two methods up does use.catch().response.ts:274,tabs.ts:62)Promise.allover every tab, confirming that one hung tab stalls all output.I didn't stand up a synthetic frozen/discarded Chrome tab to reproduce the CDP hang end-to-end — that condition is hard to force deterministically — but the code paths you point at are exactly as described, so the failure mode is unambiguous. Your suggested fix (bound the per-tab header with a small timeout/
.catch()and fall back toTargetInfo.title/url, rendering the tab as unresponsive) is the right shape.Triaged by the Playwright bot · run
yury-s commentedon Jul 9, 2026
Related bug #41093 and the fix that we ended up not landing #41128
krishpranav commentedon Aug 12, 2026
hi, can i work on this issue?
test(chromium): connect over cdp to a browser with a discarded tab