← Blog

BLOG

Playwright ConnectOverCDP Tutorial: Remote Chrome in Practice

A hands-on Playwright connectOverCDP tutorial: connect to remote Chrome over CDP, handle sessions, and run production browser automation.

September 19, 20268 min readRemote Browser

# Playwright ConnectOverCDP Tutorial: Remote Chrome in Practice

This Playwright connectOverCDP tutorial walks through connecting a Playwright script to a remote Chrome instance over the Chrome DevTools Protocol (CDP) instead of launching a local browser. If you have ever needed to drive a browser that already exists — on another machine, in a container, or behind a hosted runtime — connectOverCDP is the API that makes it possible. We will cover the exact call signature, what you get back, how contexts and pages behave, and the production details that decide whether this works reliably at scale. By the end you will have a working TypeScript example and a clear picture of when to use CDP versus Playwright's own server protocol.

What connectOverCDP actually does

browserType.connectOverCDP(endpointURL) opens a CDP connection to a running Chromium-based browser and returns a Browser object. The key word is *connect*. You are not launching anything. The browser process already exists, and Playwright attaches to it as a client.

That distinction matters because it changes the lifecycle model. With chromium.launch(), Playwright owns the process: it starts it, and browser.close() kills it. With connectOverCDP, the browser outlives your script. Closing the connection detaches your client; it does not necessarily terminate the remote browser.

The endpoint is a WebSocket URL, typically something like ws://host:port/devtools/browser/<id>. You can also pass an HTTP endpoint and Playwright will resolve the WebSocket URL from it. The remote browser must have been started with remote debugging enabled — for Chrome that means a --remote-debugging-port flag or an equivalent configuration in a hosted environment.

One constraint worth stating up front: connectOverCDP is Chromium-only. Firefox and WebKit do not implement the CDP surface Playwright relies on here. If you need cross-browser coverage, use Playwright's native protocol instead. For everything Chromium, CDP is the standard bridge.

A minimal TypeScript example

Here is the smallest useful version. It connects, grabs the default context, opens a page, and reads the title.

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

async function run(): Promise<void> {
  // The endpoint comes from your remote browser provider or your own
  // Chrome instance started with --remote-debugging-port=9222.
  const endpoint = process.env.CDP_ENDPOINT;
  if (!endpoint) {
    throw new Error('CDP_ENDPOINT is not set');
  }

  const browser: Browser = await chromium.connectOverCDP(endpoint, {
    timeout: 30_000,
  });

  // A remote browser usually has at least one context already.
  const contexts: BrowserContext[] = browser.contexts();
  const context: BrowserContext =
    contexts.length > 0 ? contexts[0] : await browser.newContext();

  const page: Page = await context.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  const title = await page.title();
  console.log('Remote page title:', title);

  await page.close();

  // Detach. This does not necessarily kill the remote browser.
  await browser.close();
}

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

Three things to notice. First, browser.contexts() may already contain contexts created by whoever started the browser — you do not always need newContext(). Second, browser.close() here means "close the CDP connection," not "shut down the remote Chrome." Third, the timeout is explicit. Remote connections fail in ways local launches do not, and a default timeout that is fine on localhost can be too aggressive across a network.

Contexts, pages, and what you inherit

When you connect to a browser someone else started, you inherit its state. That is the point, and it is also the risk.

  • Existing contexts: browser.contexts() returns everything already open. If a previous session left tabs behind, you will see them.
  • Existing pages: each context exposes context.pages(). Iterate before assuming a clean slate.
  • Cookies and storage: whatever the profile holds is available. This is how you get logged-in sessions without replaying a login flow.
  • Extensions and flags: the remote browser was launched with specific arguments. You cannot change them from the client side.

If you need isolation, create a fresh context with browser.newContext(). Contexts are the isolation boundary in Playwright, and over CDP they behave the same way — separate cookies, separate storage, separate pages. What you cannot do is change the browser-level configuration after the fact.

For agent workloads, this is usually the right pattern: one remote browser, one context per task, close the context when the task finishes. It keeps sessions from bleeding into each other without paying the cost of a full browser restart.

CDP versus Playwright's native protocol

It is worth being precise about the trade-off, because "just use CDP" is not always the right answer.

DimensionconnectOverCDPconnect (Playwright protocol)
Browser supportChromium onlyChromium, Firefox, WebKit
What you connect toAny CDP-speaking browserA Playwright server process
Process lifecycleYou attach to an existing browserServer manages the browser
Context isolationManual, via newContext()Built into the server model
Best fitAttaching to live/hosted ChromeFull cross-browser test suites
Setup requirementRemote debugging enabledPlaywright server running

The practical rule: if your target is Chromium and the browser already exists — a hosted runtime, a long-lived container, a user's live session — use connectOverCDP. If you need Firefox or WebKit, or you want Playwright to own the whole stack, use the native protocol.

There is a middle path too. Some teams run a Playwright server and connect with connect(), which gives them the isolation model without managing browser processes directly. That is a different trade-off, and it is worth reading the Playwright CDP documentation alongside the Chrome DevTools Protocol reference before committing.

Running against a hosted remote browser

The reason most people reach for connectOverCDP in production is that they do not want to run Chrome themselves. Managing a fleet of browser processes — patching Chromium, handling crashes, routing proxies, keeping sessions alive across deploys — is real infrastructure work.

A hosted runtime like Remote Browser exposes a CDP endpoint you paste into the same connectOverCDP call. The script above does not change. What changes is what sits behind the endpoint: session isolation, persistent profiles, configurable browser settings, and a live viewer for debugging. You can see the connection model in the documentation and the current usage terms on the pricing page.

The workflow looks like this:

  1. Request a session from the runtime's API and receive a CDP WebSocket URL.
  2. Pass that URL to chromium.connectOverCDP().
  3. Run your automation against the returned Browser.
  4. Close the connection and release the session.

Because the endpoint is just a URL, the same code works against a local Chrome, a container you control, or a hosted session. That portability is the main argument for CDP as an integration layer.

If you are building agent-style workloads rather than fixed test scripts, the remote browser for AI agents post covers how sessions, profiles, and proxies fit together. The connection mechanics are identical; the difference is how you manage session lifetime.

Production details that decide reliability

Getting a connection once is easy. Keeping it stable across many runs is where the real work is.

Handle reconnection explicitly. CDP connections drop. Network blips, idle timeouts, and browser crashes all sever the socket. Wrap your connection logic so a failure produces a clear error rather than a hung script. Playwright emits a disconnected event on the Browser object — listen for it.

Set timeouts deliberately. The default connectOverCDP timeout is fine for localhost and often too short for a remote endpoint. Pass an explicit timeout and tune it against your actual network path.

Do not assume a clean browser. Always inspect browser.contexts() and context.pages() before acting. A hosted session may be reused, and leftover tabs will confuse selectors that assume a single page.

Close what you open. Pages and contexts you create should be closed when the task ends. Leaked pages accumulate and eventually degrade the session.

Separate connection from task logic. Your connection helper should return a Browser and nothing else. Task code should not know whether the browser is local or remote. This makes it trivial to swap runtimes during development.

Log the endpoint, not the credentials. CDP URLs often embed tokens. Log a redacted form so debugging does not leak access.

These are not exotic requirements. They are the difference between a demo and something you can run on a schedule.

Common failure modes

A few errors show up repeatedly when people first use connectOverCDP.

  • `connect ECONNREFUSED`: nothing is listening at the endpoint. The remote browser is not running, or the port is wrong.
  • `Unexpected server response: 404`: the HTTP endpoint resolved but the WebSocket path is wrong. Check whether your provider expects the full ws:// URL or a base HTTP URL.
  • Hangs on connect: usually a firewall or proxy between you and the endpoint. Test with a plain WebSocket client before blaming Playwright.
  • `Target closed` mid-run: the remote browser crashed or the session expired. This is a lifecycle problem, not a Playwright bug.
  • Selectors fail on the wrong page: you attached to an existing context with multiple tabs. Enumerate pages first.

Most of these are diagnosable in under a minute once you know what to look for. The remote browser online guide covers the environment side of this in more depth.

When to use connectOverCDP

Use it when the browser already exists and you need to drive it. That covers hosted runtimes, long-lived containers, debugging sessions, and any workflow where preserving browser state matters more than starting clean.

Do not use it when you need Firefox or WebKit, when you want Playwright to manage the full browser lifecycle, or when your test suite depends on launch-time configuration you cannot set on a remote instance.

For teams running browser automation at any real volume, the connection API is the easy part. The hard part is everything around it: session management, isolation, proxies, and observability. A hosted runtime handles that layer so your code stays focused on the task. Start with the documentation to see how sessions are created and connected, then decide whether CDP or the native protocol fits your stack.