← Blog

BLOG

Playwright ConnectOverCDP Persistent Browser Session

Learn how Playwright connectOverCDP enables persistent browser sessions, when state survives reconnects, and how to run it against hosted Chromium.

October 8, 20268 min readRemote Browser

# Playwright ConnectOverCDP Persistent Browser Session

browserType.connectOverCDP() is the Playwright API that attaches to a running Chromium instance over the Chrome DevTools Protocol instead of launching a new browser. The reason people search for a Playwright connectOverCDP persistent browser session is almost always the same: they want cookies, localStorage, and logged-in state to survive across script runs, reconnects, and process restarts. That is possible, but only if you understand what actually persists and what does not.

This guide covers how connectOverCDP behaves, where session state lives, how to keep it alive in production, and how a hosted Chromium runtime changes the equation. If you want the broader runtime picture first, see Remote Browser for AI agents.

What connectOverCDP actually does

connectOverCDP(endpointURL) returns a Browser object bound to an already-running Chromium process. You pass a CDP endpoint — typically an HTTP URL like http://127.0.0.1:9222 or a WebSocket URL like ws://host:9222/devtools/browser/<id> — and Playwright attaches to it.

Two behaviors matter for persistence:

  • The browser process is not owned by Playwright. browser.close() on a CDP connection disconnects the client. It does not necessarily terminate the remote browser. That distinction is the foundation of persistence.
  • Contexts and pages already exist. With connectOverCDP, you call browser.contexts() to get existing contexts rather than browser.newContext(). The default context holds the profile state — cookies, localStorage, IndexedDB, service workers.

The official Playwright CDP documentation notes that connectOverCDP is Chromium-only and that Firefox support is limited. If you are on Firefox or WebKit, this API is not your path.

Where persistent state actually lives

A common misconception: that connectOverCDP itself creates persistence. It does not. It attaches to a browser whose state is already on disk (or in memory). Persistence comes from the browser's user data directory.

State typePersists across reconnect?Persists across browser restart?Notes
CookiesYesYes, if user data dir is reusedTied to profile
localStorage / sessionStorageYeslocalStorage yes; sessionStorage nosessionStorage is per-tab
IndexedDBYesYesSame origin rules apply
Open tabs / pagesYes, while process runsNoLost on process exit
In-flight network requestsNoNoDropped on disconnect
CDP session IDsNoNoNew session per connect
Auth tokens in memory (JS vars)NoNoLost on navigation or restart

The practical takeaway: if you want a persistent session, you need a persistent profile — a user data directory that the browser reuses — plus a browser process that stays alive between your script runs. connectOverCDP is the client-side half of that contract.

The minimal persistent-session pattern

Here is a TypeScript example that connects to a remote Chromium, reuses the existing context, and verifies that state survives a reconnect.

import { chromium, Browser, BrowserContext, Page } from 'playwright';

const CDP_ENDPOINT = process.env.CDP_ENDPOINT!; // e.g. wss://.../cdp

async function attach(): Promise<{ browser: Browser; context: BrowserContext; page: Page }> {
  const browser = await chromium.connectOverCDP(CDP_ENDPOINT, {
    timeout: 30_000,
  });

  // Reuse the default context — this is where the persistent profile lives.
  const context = browser.contexts()[0] ?? (await browser.newContext());
  const page = context.pages()[0] ?? (await context.newPage());

  return { browser, context, page };
}

async function main() {
  const { browser, context, page } = await attach();

  await page.goto('https://example.com/dashboard');

  // Read state that should survive across runs.
  const cookies = await context.cookies();
  const token = await page.evaluate(() => window.localStorage.getItem('auth_token'));

  console.log('cookies:', cookies.length, 'token present:', Boolean(token));

  // Disconnect without killing the remote browser.
  await browser.close();
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Two details are easy to get wrong:

  1. Do not call `context.close()` if you want the profile to persist. Closing the context can flush or discard state depending on how the runtime is configured.
  2. Do not call `browser.close()` expecting the remote process to die. On a CDP connection, close() disconnects. Whether the remote browser terminates is a property of the runtime, not Playwright.

Persistent sessions vs. fresh sessions: when to use which

Not every workload should reuse a profile. Reusing state is a trade-off between continuity and isolation.

RequirementPersistent profileFresh context per run
Stay logged in across runsRequiredNot possible without re-auth
Avoid cross-task contaminationRisky — state leaksSafe
Parallel runs on same accountDangerous — session conflictsSafe if separate accounts
Reproduce a bug from a prior runUsefulHarder
Compliance / data isolationNeeds explicit scopingDefault-safe
Speed to first meaningful actionFaster (already authenticated)Slower (login flow)

The rule of thumb: persist the identity, isolate the task. Use a persistent profile per account or per tenant, and create a fresh context for each task if the runtime supports it. If your runtime only exposes one default context, treat the whole browser as the isolation boundary.

Why local connectOverCDP setups break in production

Running chrome --remote-debugging-port=9222 on your laptop and connecting over CDP works fine for development. It fails in production for predictable reasons:

  • The browser process dies with the host. A container restart, a deploy, or an OOM kill takes the profile with it unless the user data directory is on durable storage.
  • Port and endpoint drift. The CDP endpoint changes when the process restarts, so hardcoded localhost:9222 breaks.
  • No concurrency control. Two scripts attaching to the same browser fight over tabs and navigation.
  • No observability. When a run fails, you have no live view and no session recording.
  • Profile corruption. Concurrent writes to the same user data directory can corrupt the profile.

These are infrastructure problems, not Playwright problems. That is the gap a hosted runtime fills. For a comparison of the two approaches, see Remote Browser online.

How a hosted Chromium runtime handles persistence

A hosted runtime like Remote Browser manages the browser process, the profile storage, and the CDP endpoint for you. The relevant properties:

  • Stable CDP endpoint per session. You get a WebSocket URL you can pass straight into connectOverCDP. The endpoint is stable for the life of the session.
  • Persistent profiles. Profile state is stored durably, so a session can be resumed rather than rebuilt. This is what makes a genuinely persistent browser session possible across worker restarts.
  • Session isolation. Each session gets its own browser process and profile, so parallel runs do not collide.
  • Live viewer. You can watch the session in real time, which matters when debugging a login flow or a stuck navigation.
  • Configurable browser settings. Proxy configuration, locale, timezone, and user-agent are set at the session level rather than patched into launch args.
  • Usage controls. Sessions are metered, so you can reason about cost per workload. Current rates are on the pricing page.

The important architectural point: with a hosted runtime, connectOverCDP becomes a reconnect operation, not a launch operation. Your script can die, redeploy, or scale down, and the browser keeps its state.

Production criteria for persistent CDP sessions

If you are evaluating whether your setup can support persistent sessions, check these:

  • Does the profile survive a client disconnect? Test it: connect, log in, disconnect, reconnect, check context.cookies().
  • Is the CDP endpoint stable across reconnects? If it changes, you need a session lookup step before connecting.
  • Can you run two sessions without state bleed? If not, you have a shared-profile problem.
  • Is there a live view or recording? Persistent sessions are harder to debug than ephemeral ones because state accumulates.
  • What happens on profile corruption? You need a way to reset a profile without losing the account.
  • How is session time metered? Persistent sessions that idle are still consuming resources. See pricing for how Remote Browser meters this.

Common failure modes and fixes

Cookies disappear after reconnect. The browser was restarted without reusing the user data directory. Fix: ensure the runtime persists the profile, not just the process.

`connectOverCDP` times out. The endpoint is wrong, the browser is not listening on the CDP port, or a proxy is blocking the WebSocket upgrade. Verify the endpoint with a raw WebSocket client before blaming Playwright.

Two scripts interfere with each other. They are attached to the same browser. Fix: one session per script, or coordinate tab ownership explicitly.

Login state works locally but not remotely. The remote profile is fresh. Fix: perform the login once against the persistent profile, then reuse it.

Session state leaks between tenants. You are sharing a profile. Fix: one profile per tenant, enforced at the runtime layer.

When connectOverCDP is the wrong tool

connectOverCDP is not the right choice when:

  • You need Firefox or WebKit. Use connect() with a Playwright server instead.
  • You need Playwright's full context isolation features. CDP-attached browsers expose contexts differently, and some newContext() options are not honored.
  • You need deterministic, hermetic test runs. Persistent state is the opposite of hermetic. Use a fresh context per test.
  • You are running short-lived, stateless scrapes. The persistence overhead buys you nothing.

For those cases, a hosted runtime still helps — you just want ephemeral sessions rather than persistent ones. The remote web browser guide covers the ephemeral path.

Putting it together

A persistent browser session with connectOverCDP is really three things working together:

  1. A browser process that stays alive between your script runs.
  2. A persistent profile that stores cookies, localStorage, and IndexedDB durably.
  3. A stable CDP endpoint your client can reconnect to.

Playwright gives you the client. The runtime gives you the other two. If you are building agents that need to stay logged in, resume tasks, or avoid re-authenticating on every run, that split is the thing to get right. You can read more about how Remote Browser handles sessions in the documentation, or see how it fits agent workloads in remote control browser.