← Blog

BLOG

Playwright ConnectOverCDP Remote Browser

Use Playwright connectOverCDP with a remote browser to drive hosted Chromium over CDP. Setup, code, trade-offs, and production criteria.

October 14, 20269 min readRemote Browser

# Playwright ConnectOverCDP Remote Browser

browserType.connectOverCDP() is the Playwright API that attaches to an already-running Chromium instance over the Chrome DevTools Protocol instead of launching a browser locally. When you point it at a remote browser, your Playwright script runs on your machine (or your worker) while the actual Chromium process, its profile, and its network egress live in a hosted environment. That split is the whole point: you get Playwright's API surface without shipping a browser binary, managing a display server, or pinning Chrome versions across every host that runs your automation.

This guide covers how connectOverCDP actually behaves, what a remote browser endpoint gives you, where the model breaks down, and the production criteria worth checking before you commit to it.

What connectOverCDP Actually Does

Playwright has two ways to reach a browser it did not launch:

  • `connectOverCDP(endpointURL)` — speaks the Chrome DevTools Protocol to a Chromium-based browser. Works with Chrome, Chromium, Edge, and other CDP-compatible builds. Returns a Browser object.
  • `connect(wsEndpoint)` — speaks Playwright's own protocol. Requires the remote side to run a Playwright server (playwright run-server). Only works Playwright-to-Playwright.

connectOverCDP is the more portable option because CDP is a browser-level protocol, not a Playwright-specific one. Anything that exposes a CDP endpoint — a local Chrome started with --remote-debugging-port, a container running headless Chromium, or a hosted browser runtime — can be driven this way. The Chrome DevTools Protocol is the underlying contract; Playwright is one of several clients that speak it.

The practical consequence: connectOverCDP is Chromium-only. If your test matrix needs Firefox or WebKit, this API is not the path — use connect() against a Playwright server, or run those browsers locally.

Why Point connectOverCDP at a Remote Browser

Launching Chromium locally is fine until it isn't. The friction shows up in predictable places:

  • Environment drift. Your laptop runs Chrome 131, CI runs 128, a colleague's container runs whatever the base image shipped. Selectors and behaviors diverge.
  • Resource contention. Each headless Chromium is a real process tree. Running twenty in parallel on one box means memory pressure, zombie processes, and flaky timeouts.
  • No persistent state. Local launches start clean. Logging into a site on every run is slow and often triggers bot defenses.
  • Egress and IP reputation. A datacenter IP from your CI provider gets challenged more often than a residential one.
  • Debugging blind spots. When a headless run fails in CI, you get a stack trace and maybe a screenshot. You cannot watch it happen.

A remote browser runtime addresses these by moving the browser process off your infrastructure. You keep the Playwright client; the runtime owns the Chromium lifecycle, the profile storage, the network path, and the observability surface. Remote Browser is one implementation of that model — hosted Chromium sessions with CDP access, persistent profiles, configurable browser and network settings, and a live viewer for watching sessions as they run.

Connecting: A Working TypeScript Example

The minimal shape is short. The details around it are where production lives.

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

// Your runtime issues a CDP endpoint per session.
// Treat this as a secret: anyone with it controls the browser.
const CDP_ENDPOINT = process.env.REMOTE_BROWSER_CDP_URL!;

async function run(): Promise<void> {
  const browser: Browser = await chromium.connectOverCDP(CDP_ENDPOINT, {
    timeout: 30_000,
  });

  // connectOverCDP attaches to an existing browser, so contexts
  // may already exist. Reuse the default one when present.
  const context: BrowserContext =
    browser.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('title:', title);

  // Do NOT call browser.close() on a shared remote browser unless
  // you intend to terminate the whole session. Disconnect instead.
  await browser.close();
}

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

Three things in that snippet matter more than they look:

  1. `browser.contexts()` may be non-empty. With launch(), you always start with zero contexts. With connectOverCDP, you are attaching to a browser that may already have contexts and pages open. Code that assumes a blank slate will misbehave.
  2. `browser.close()` semantics differ. On a launched browser it kills the process. On a connected browser it closes the connection — but depending on the runtime, it may also terminate the session. If you want to detach and leave the session alive, check your runtime's behavior and prefer an explicit disconnect path.
  3. The endpoint is a credential. A CDP endpoint grants full control of the browser, including reading cookies and navigating anywhere. Store it in a secret manager, not in a repo.

What a Remote Browser Endpoint Should Provide

Not every "remote browser" is the same. When you evaluate one for connectOverCDP, these are the properties that determine whether it holds up:

CapabilityWhy it mattersWhat to check
CDP endpoint per sessionIsolation between concurrent jobsEndpoint is unique per session, not shared
Persistent profilesAvoid re-authenticating every runProfile survives session teardown; opt-in per session
Session isolationOne job cannot see another's cookiesSeparate browser process or at least separate profile
Live viewerDebug without reproducing locallyViewport streaming, not just screenshots
Proxy / network configEgress IP and geo controlPer-session proxy, not global
Configurable browser settingsLocale, timezone, UA, viewportSet at session creation, not patched mid-run
Usage controlsPredictable cost and concurrencyPer-session limits, visible metering
Playwright/Puppeteer/Selenium compatReuse existing codeCDP endpoint works with all three

The last row is worth emphasizing. Because connectOverCDP is a CDP client, the same endpoint works from Puppeteer (puppeteer.connect({ browserWSEndpoint })) and from Selenium's CDP bridge. You are not locked into Playwright by choosing a CDP endpoint — you are locked into Chromium.

connectOverCDP vs. Other Connection Models

It helps to place connectOverCDP against the alternatives, because the right choice depends on what you are optimizing for.

ApproachProtocolBrowser supportState persistenceBest for
chromium.launch()Local processChromiumNone by defaultLocal dev, quick scripts
chromium.launchPersistentContext()Local processChromiumLocal profile dirLocal runs needing login state
connectOverCDP()CDPChromium onlyDepends on runtimeRemote Chromium, mixed clients
connect()Playwright protocolChromium, Firefox, WebKitDepends on serverCross-browser remote runs
Selenium Grid / Remote WebDriverWebDriverAll majorDepends on gridLegacy test suites

The trade-off is clear: connectOverCDP buys you portability across CDP-speaking clients and a simpler remote setup, at the cost of cross-browser coverage. If your automation is Chromium-only — which most AI agent and scraping workloads are — that cost is zero.

If you need the broader picture of how remote runtimes fit into agent architectures, see Remote Browser for AI Agents.

Production Criteria Before You Commit

The API call is easy. The operational questions are not. Work through these before you move a workload onto a remote browser:

Session lifecycle. How does a session start and end? Is there an explicit create/terminate API, or does the endpoint appear and disappear? What happens to a session when your client crashes mid-run — does it leak, or does it time out? Leaked sessions are the most common cost surprise in this model.

Profile persistence. If you need to stay logged in across runs, the profile must survive session teardown and be reattachable. Confirm whether profiles are per-session, per-user, or global, and whether you can delete them on demand. Persistent profiles are also a security surface: a profile holds live cookies, so treat profile IDs as sensitive.

Concurrency and isolation. Two sessions must not share cookies, storage, or network identity. Ask whether isolation is at the process level or the profile level — process-level is stronger.

Network path. If target sites are sensitive to IP reputation, per-session proxy configuration matters. "Configurable browser settings" should include locale, timezone, and user agent at minimum; be skeptical of any claim you cannot verify in the API.

Observability. A live viewer changes debugging from guesswork to inspection. Screenshots at failure time are useful; watching the session in real time is better. See Remote Control Browser for how live control fits into the workflow.

Cost model. Remote browser runtimes typically meter by browser-hour, sometimes plus network traffic. The number that matters is cost per completed task, not cost per hour — a cheap session that fails and retries is more expensive than a pricier one that succeeds. Check pricing for current rates rather than assuming.

Common Failure Modes

Most connectOverCDP problems fall into a handful of buckets:

  • Endpoint unreachable. Usually a firewall, a missing wss:// scheme, or an expired session. Verify the endpoint with a raw CDP call before blaming Playwright.
  • Timeout on connect. The default connect timeout is short. Pass an explicit timeout and make sure your runtime has finished provisioning the session.
  • Stale context assumptions. Code written for launch() that calls newContext() unconditionally will create a second context and miss the logged-in one. Always check browser.contexts() first.
  • Version mismatch. CDP evolves. A very new Playwright against an old Chromium build (or vice versa) can produce confusing protocol errors. Pin both sides.
  • Session leaks. Client crashes without closing the session. Use try/finally and rely on runtime-side timeouts as a backstop.

When connectOverCDP Is the Wrong Tool

Be honest about the boundaries:

  • You need Firefox or WebKit. Use connect() against a Playwright server, or run those browsers locally.
  • You need browser-level isolation guarantees you cannot verify. If the runtime shares a browser process across tenants, CDP endpoints may not be as isolated as they look.
  • Your workload is a single short script. The overhead of provisioning a remote session may exceed the benefit. Local launch() is fine.
  • You need deterministic, offline execution. A remote browser depends on network reachability to the runtime. Air-gapped CI is not a fit.

For a broader comparison of local versus hosted execution, Remote Browser Online walks through the setup differences.

Practical Checklist

Before shipping a connectOverCDP workload to production:

  • Store the CDP endpoint in a secret manager; rotate per session.
  • Pass an explicit connect timeout sized to your runtime's provisioning time.
  • Reuse browser.contexts()[0] when present; never assume a blank browser.
  • Wrap sessions in try/finally so a crash does not leak a session.
  • Decide explicitly whether browser.close() should terminate or detach.
  • Pin Playwright and Chromium versions together.
  • Verify isolation by running two sessions concurrently and checking cookies do not cross.
  • Instrument cost per completed task, not just per session.
  • Use a live viewer for the first week of any new workload.

The documentation covers the session API, profile handling, and viewer in more detail.

The Short Version

connectOverCDP is the right primitive when you want Playwright's API against a Chromium browser you do not host. It is Chromium-only, it attaches to an existing browser rather than creating one, and its production quality depends almost entirely on the runtime behind the endpoint — isolation, profile persistence, network configuration, and observability. Get those right and the API call is the easy part. Get them wrong and no amount of retry logic will save the workload.