BLOG
Playwright Connect To Remote Browser Over CDP
Learn how to use Playwright connect to remote browser over CDP: endpoints, code, session handling, and production trade-offs for hosted Chromium.
# Playwright Connect To Remote Browser Over CDP
To connect Playwright to a remote browser over CDP, you call chromium.connectOverCDP() with a WebSocket endpoint that exposes the Chrome DevTools Protocol. That single call replaces chromium.launch() and points your existing Playwright code at a browser running somewhere else — a container, a VM, or a hosted runtime like Remote Browser. This guide covers the endpoint format, working TypeScript, session and context handling, and the production criteria that separate a demo from a reliable setup.
If you already run Playwright locally, the migration is small: swap the launch call, keep your locators and assertions. The hard parts are not the API — they are session lifecycle, context reuse, and knowing when CDP is the right transport versus Playwright's own server protocol.
What "connect over CDP" actually means
CDP is the protocol Chrome exposes for programmatic control. When Chrome runs with a remote debugging port, it publishes a WebSocket endpoint. Any client that speaks CDP — Playwright, Puppeteer, or a raw WebSocket library — can attach to that endpoint and drive the browser.
Playwright's connectOverCDP() is a thin, well-tested wrapper around that. It connects to the endpoint, discovers existing browser contexts and pages, and returns a Browser object you use exactly like one from launch(). The Chrome DevTools Protocol documentation describes the underlying domains; Playwright handles the plumbing.
Two things matter for planning:
- CDP is Chromium-only in practice.
connectOverCDPtargets Chromium-based browsers. If you need Firefox or WebKit, use Playwright'sconnect()with a Playwright server instead. - You attach to a running browser. There is no launch step. The remote side owns process lifecycle, and you own session lifecycle.
The endpoint: what you actually pass in
A CDP endpoint is a WebSocket URL, typically shaped like:
wss://<host>/<session-path>?token=<credential>Hosted runtimes usually return this URL from a session-creation API call. You create a session, get an endpoint, connect, do work, then release the session. The endpoint is scoped to that session, so it is not a long-lived global address.
Practical rules:
- Treat the endpoint as a secret. It grants full control of that browser.
- Do not hardcode it. Fetch it per run from your runtime's API.
- Expect it to expire. Sessions have a lifetime; reconnect logic should create a new session rather than retry a dead endpoint forever.
Remote Browser exposes CDP endpoints per session, alongside a live viewer and configurable browser settings. See the documentation for the current session API shape.
Minimal TypeScript example
The following connects to a remote CDP endpoint, reuses an existing context if one is present, and runs a simple navigation. It assumes you have a session endpoint in CDP_ENDPOINT.
import { chromium, Browser, BrowserContext, Page } from 'playwright';
async function run(): Promise<void> {
const endpoint = process.env.CDP_ENDPOINT;
if (!endpoint) throw new Error('CDP_ENDPOINT is not set');
let browser: Browser | undefined;
try {
// Attach to the remote Chromium instance over CDP.
browser = await chromium.connectOverCDP(endpoint, {
timeout: 30_000,
});
// A remote browser may already have contexts (e.g. a persistent profile).
const contexts: BrowserContext[] = browser.contexts();
const context: BrowserContext =
contexts.length > 0 ? contexts[0] : await browser.newContext();
const page: Page = context.pages()[0] ?? (await context.newPage());
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const title = await page.title();
console.log('Remote page title:', title);
// Do not close the browser here if the runtime owns its lifecycle.
// Close only the context you created, and only if you created it.
} finally {
// connectOverCDP does not kill the remote browser on close().
// Closing the client just detaches.
await browser?.close();
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});Three details worth calling out:
- `browser.contexts()` may be non-empty. With persistent profiles, the remote browser can arrive with contexts already open. Reusing them preserves cookies and storage.
- `browser.close()` detaches, it does not terminate. The remote process keeps running until the runtime reaps the session. Budget for that.
- Set an explicit timeout. Network distance to a remote endpoint is real; the default may be too tight or too loose for your workload.
CDP vs Playwright's own connect
Playwright offers two ways to reach a remote browser. They are not interchangeable.
| Criterion | connectOverCDP() | connect() (Playwright server) |
|---|---|---|
| Protocol | Chrome DevTools Protocol | Playwright's own wire protocol |
| Browser support | Chromium-based only | Chromium, Firefox, WebKit |
| Attaches to existing browser | Yes | Yes, if server exposes it |
| Access to existing contexts/pages | Yes | Yes |
| Best fit | Hosted Chromium, CDP-native runtimes | Cross-browser grids, Playwright-native infra |
| Typical use | AI agents, scraping, hosted sessions | CI matrices, multi-engine testing |
If your target is a hosted Chromium runtime that advertises a CDP endpoint, connectOverCDP() is the direct path. If you need Firefox or WebKit coverage, plan for connect() and a Playwright server instead. For a broader look at remote browser options, see Remote Browser for AI Agents.
Session and context lifecycle in production
The API call is the easy part. Most failures in remote CDP setups come from lifecycle mistakes.
Create one session per logical task. Do not share a CDP endpoint across concurrent workers. Sessions are isolated; sharing one means sharing cookies, storage, and navigation state, which produces nondeterministic bugs.
Decide context reuse deliberately. Two patterns:
- *Fresh context per task*: call
browser.newContext()and close it when done. Clean state, predictable. - *Persistent profile*: reuse the context the runtime provides. Preserves login state and cookies across sessions. Useful for agents that must stay authenticated.
Handle disconnects explicitly. A dropped WebSocket does not always mean the session died. Check whether the endpoint is still valid before reconnecting; otherwise you leak sessions. A simple pattern is to attempt one reconnect, then create a fresh session on failure.
Release sessions. If your runtime bills per browser-hour, orphaned sessions cost money. Close what you open. Remote Browser meters usage and exposes controls for this; current terms are on the pricing page.
When CDP is the right choice
CDP is a strong default for hosted Chromium, but it is not universal.
Use connectOverCDP() when:
- Your runtime exposes a CDP endpoint and you want minimal client code.
- You need to attach to a browser that is already running, including one with an existing profile.
- You are driving Chromium-based agents, scrapers, or test harnesses.
- You want Puppeteer and Playwright clients to speak to the same browser.
Avoid it when:
- You need Firefox or WebKit. Use
connect(). - You need Playwright-specific features that CDP does not surface cleanly.
- Your runtime only offers a Playwright server endpoint, not raw CDP.
For a side-by-side on hosted versus self-managed infrastructure, Remote Browser Online covers the operational trade-offs.
Common failure modes and fixes
Connection refused or 404 on the endpoint. The session expired or was never created. Re-fetch the endpoint from your runtime API.
`connectOverCDP` hangs. Usually a network path issue — the endpoint is reachable from the runtime but not from your worker. Verify egress rules and that you are using the correct scheme (wss:// vs ws://).
Pages appear empty after connect. You may be looking at a context that has not navigated yet. Enumerate context.pages() and check page.url() before assuming failure.
State leaks between runs. You reused a context when you meant to create a fresh one. Switch to newContext() per task.
Timeouts under load. Remote round-trips add latency. Raise navigation and action timeouts, and avoid tight polling loops that amplify network cost.
Production checklist
Before you ship a CDP-based Playwright setup:
- Endpoints are fetched per run, never hardcoded.
- One session per task; no cross-worker sharing.
- Context strategy is explicit (fresh vs persistent).
- Reconnect logic distinguishes dead sessions from transient drops.
- Sessions are released on success and failure paths.
- Timeouts are tuned for network distance.
- Credentials are stored as secrets, not in source.
- Usage and cost are observable — see pricing for how metering works.
Where Remote Browser fits
Remote Browser provides hosted Chromium sessions with CDP access, Playwright/Puppeteer/Selenium compatibility, a live viewer for debugging, persistent profiles, configurable browser settings, and session isolation. You create a session, get a CDP endpoint, and connect with the code above. The live viewer is useful when an agent stalls and you need to see the actual page state rather than infer it from logs.
It is not a replacement for Playwright's cross-browser testing story, and it is not a magic fix for sites that block automation. It is a runtime layer: it removes the need to run and scale Chromium yourself, and it gives you a consistent endpoint to connect to. For the broader context on why that layer exists, see Remote Web Browser and Remote Control Browser.
Summary
Connecting Playwright to a remote browser over CDP is one call: chromium.connectOverCDP(endpoint). The engineering effort lives in session lifecycle, context reuse, reconnect handling, and cost visibility. Get those right and the transport becomes boring — which is exactly what you want in production. Start with the documentation to see the current session API, and check pricing before you scale concurrency.