microsoft / microsoft/playwright

[Bug] playwright-cli: one discarded (Memory Saver) tab makes every command time out — tab header rendering awaits page.title() without timeout

Open
#41,714 3 comments 1 reaction 1 assignee View on GitHub

@dgozman is already working on this.

Since Jul 10, 2026.

v1.64
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.

  1. 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).
  2. 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.)
  3. playwright-cli attach --cdp=http://localhost:9222 → succeeds.
  4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.