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.
# 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 callbrowser.contexts()to get existing contexts rather thanbrowser.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 type | Persists across reconnect? | Persists across browser restart? | Notes |
|---|---|---|---|
| Cookies | Yes | Yes, if user data dir is reused | Tied to profile |
| localStorage / sessionStorage | Yes | localStorage yes; sessionStorage no | sessionStorage is per-tab |
| IndexedDB | Yes | Yes | Same origin rules apply |
| Open tabs / pages | Yes, while process runs | No | Lost on process exit |
| In-flight network requests | No | No | Dropped on disconnect |
| CDP session IDs | No | No | New session per connect |
| Auth tokens in memory (JS vars) | No | No | Lost 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:
- 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.
- 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.
| Requirement | Persistent profile | Fresh context per run |
|---|---|---|
| Stay logged in across runs | Required | Not possible without re-auth |
| Avoid cross-task contamination | Risky — state leaks | Safe |
| Parallel runs on same account | Dangerous — session conflicts | Safe if separate accounts |
| Reproduce a bug from a prior run | Useful | Harder |
| Compliance / data isolation | Needs explicit scoping | Default-safe |
| Speed to first meaningful action | Faster (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:9222breaks. - 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:
- A browser process that stays alive between your script runs.
- A persistent profile that stores cookies, localStorage, and IndexedDB durably.
- 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.