microsoft / microsoft/playwright
[Bug] playwright-cli: one discarded (Memory Saver) tab makes every command time out — tab header rendering awaits page.title() without timeout
@dgozman is already working on this.
Since Jul 10, 2026.
- Dominant language
- TypeScript
- Stars
- 96.3k
- Forks
- 6.5k
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 180
Description
Version
@playwright/cli 0.1.15 (bundled playwright-core 1.62.0-alpha-2026-06-29)
Steps to reproduce
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.) playwright-cli attach --cdp=http://localhost:9222→ succeeds.- 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
await this._raceAgainstModalStates(async () => {
title = await this.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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.