← Blog

BLOG

Playwright Connect To Remote Chrome: CDP Setup Guide

Learn how to use Playwright connect to remote Chrome over CDP. Covers endpoint formats, code examples, production trade-offs, and hosted runtime options.

October 14, 20269 min readRemote Browser

# Playwright Connect To Remote Chrome

To connect Playwright to a remote Chrome instance, you call chromium.connectOverCDP() with a WebSocket or HTTP endpoint that exposes the Chrome DevTools Protocol. That single call replaces chromium.launch() and gives you a Browser object backed by a Chrome process running somewhere else — a container, a VM, or a hosted browser runtime. This guide covers the endpoint formats, the code, the failure modes, and the production criteria for choosing between self-hosted and managed remote Chrome.

If you already know you want a managed endpoint, Remote Browser provides hosted Chromium sessions with CDP access, persistent profiles, and a live viewer. The rest of this article explains what happens underneath so you can debug it when it breaks.

Why connect instead of launch

chromium.launch() starts a browser process on the same machine as your Playwright script. That works until it doesn't:

  • Resource contention. Each Chromium instance consumes 150–400 MB of RAM depending on page complexity. Ten parallel workers on a 4 GB CI runner will OOM.
  • Environment drift. Your local Chrome version, fonts, and OS libraries differ from production. Tests pass locally and fail in CI for reasons unrelated to your code.
  • No session persistence. A launched browser dies with the process. Cookies, localStorage, and logged-in state vanish between runs.
  • No observability. You cannot watch a headless browser on a CI runner. Debugging a flaky selector means adding screenshots and re-running.

Connecting to a remote Chrome decouples the browser lifecycle from your script lifecycle. The browser can outlive your process, run on hardware sized for it, and be inspected while it works.

Endpoint formats: WebSocket vs HTTP

Chrome exposes CDP over two transports. Playwright's connectOverCDP() accepts both.

FormatExampleUse case
WebSocketws://host:9222/devtools/browser/<id>Full CDP, bidirectional events, required for most automation
HTTPhttp://host:9222Playwright resolves the WebSocket URL from /json/version

The HTTP form is convenient because you don't need to know the browser GUID. Playwright fetches http://host:9222/json/version, reads the webSocketDebuggerUrl field, and connects. The WebSocket form skips that round trip and is what most hosted providers hand you directly.

A raw Chrome started with --remote-debugging-port=9222 serves both. Note that recent Chrome versions require --remote-debugging-address=0.0.0.0 if you want to reach it from outside the container, and Chrome refuses to start remote debugging against the default profile directory in some builds — use --user-data-dir explicitly.

The minimal connection

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

async function connectToRemoteChrome(endpoint: string): Promise<void> {
  // connectOverCDP accepts an HTTP or WebSocket CDP endpoint.
  const browser: Browser = await chromium.connectOverCDP(endpoint);

  // A remote browser may already have contexts (e.g. a persistent profile).
  // Reuse the default context instead of creating a new one when it exists.
  const context: BrowserContext =
    browser.contexts()[0] ?? (await browser.newContext());

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

  console.log('title:', await page.title());
  console.log('version:', browser.version());

  // Do NOT call browser.close() on a shared remote browser unless you own it.
  await page.close();
}

connectToRemoteChrome(process.env.CDP_ENDPOINT!).catch((err) => {
  console.error('CDP connection failed:', err);
  process.exit(1);
});

Three details matter here.

`browser.contexts()` may not be empty. When you connect to a browser that is already running, it may have contexts created by a previous session or by the runtime itself. Calling newContext() unconditionally leaks contexts and can break profile persistence.

`browser.close()` is destructive. On a launched browser it shuts down the process you own. On a connected browser it sends Browser.close over CDP, which terminates the remote Chrome. If the runtime shares that browser across sessions, you just killed someone else's work. Close pages and contexts; let the runtime reap the browser.

`connectOverCDP` is Chromium-only. Playwright's CDP support targets Chromium-based browsers. Firefox and WebKit do not implement CDP, so connectOverCDP will not work against them. If you need cross-browser remote execution, use Playwright's own server protocol instead — see the Playwright CDP documentation for the exact contract.

What you get and what you don't

Connecting over CDP gives you most of the Playwright API, but not all of it.

Available: page.goto, locators, page.evaluate, network interception via page.route, context.addCookies, page.screenshot, tracing, and browser.newContext().

Limited or unavailable:

  • browser.newContext() on a connected browser creates a context inside the remote process, but context-level options like recordVideo and recordHar depend on the remote runtime's filesystem and may not be retrievable.
  • browserType.launchPersistentContext() is a launch-time API. For persistence over CDP, you rely on the remote browser's own profile directory.
  • Browser binaries are not downloaded or version-managed by your script. The remote side owns the Chrome version, and a version mismatch between your Playwright client and the remote Chrome can surface as missing CDP domains.

That last point is the most common source of "it worked yesterday" bugs. Playwright pins a Chromium revision per release. If your remote Chrome is significantly newer or older, some CDP methods return method not found. Pin both sides, or use a runtime that lets you select the browser version.

Self-hosted vs hosted remote Chrome

You can run Chrome yourself in a container and expose port 9222. You can also connect to a managed endpoint. The trade-off is operational, not architectural — both speak CDP.

CriterionSelf-hosted ChromeHosted remote Chrome
SetupDockerfile, Chrome flags, port exposure, health checksPaste a connection URL
ScalingYou manage orchestration, scheduling, and teardownRuntime handles session lifecycle
Session persistenceYou mount and back up profile volumesProfile persistence is a runtime feature
Proxy configurationYou pass --proxy-server and manage credentialsConfigurable per session
ObservabilityYou build screenshot/video capture and log shippingLive viewer and session logs typically included
Cost modelCompute + egress + your timeMetered per browser-hour; see /pricing
DebuggingSSH into the containerAttach a viewer or connect a second CDP client

Self-hosting is the right call when you have strict data residency requirements, an existing container platform, and engineers who own browser infrastructure. It is the wrong call when browser uptime is not your product.

Hosted runtimes earn their cost in the failure modes they absorb: zombie Chrome processes, profile corruption, rotating outbound IPs, and the long tail of "the browser started but CDP never came up."

Production criteria for a remote Chrome endpoint

Before you commit to an endpoint, verify these properties. They are the difference between a demo and something that runs unattended.

1. Connection stability under load. Open 20 concurrent CDP connections and hold them for 10 minutes. Watch for dropped WebSockets. CDP has no built-in reconnect; a dropped socket means a dead Browser object and a failed job.

2. Session isolation. Two sessions must not share cookies or storage unless you explicitly want them to. Verify by logging into the same site in two sessions and checking that the second does not inherit the first's auth state.

3. Profile persistence semantics. Know exactly when a profile is written back. Some runtimes persist on graceful close only; a crashed session loses state. Others snapshot continuously. This determines whether you can resume a half-finished workflow.

4. Proxy and network controls. If your workload touches geo-restricted content, confirm the proxy is applied at the browser level, not just the HTTP client level. A proxy set on your Playwright script does not route the browser's own requests.

5. Timeouts and reaping. A session that nobody closes should be reaped. Ask what the idle timeout is and whether it is configurable.

6. Version control. Can you pin the Chrome version? Can you upgrade on your schedule rather than the provider's?

For a deeper look at how these map to agent workloads specifically, see Remote Browser for AI agents.

Debugging a failed connection

connectOverCDP throws fast and the error messages are terse. Work through these in order.

`connect ECONNREFUSED` — nothing is listening. Check the port, the bind address (0.0.0.0 vs 127.0.0.1), and container network mode.

`Unexpected server response: 404` — you hit an HTTP endpoint that isn't a CDP endpoint. Confirm /json/version returns JSON with a webSocketDebuggerUrl.

`WebSocket is closed before the connection is established` — usually an auth failure or a proxy stripping the Upgrade header. Hosted endpoints often require a token in the URL or a header; check the provider's connection string format.

Connection succeeds, then `Target closed` — the browser was reaped mid-session. Either your idle timeout is too aggressive or the runtime enforces a hard session cap.

`Protocol error (X): Method not found` — version mismatch between your Playwright client and the remote Chrome. Pin the Playwright version or select a matching browser build.

A quick sanity check that bypasses Playwright entirely:

curl -s http://your-endpoint:9222/json/version | jq .

If that returns a webSocketDebuggerUrl, the endpoint is alive and the problem is on the Playwright side.

When to use a hosted runtime

The decision is straightforward. Connect to a hosted remote Chrome when:

  • You run more than a handful of concurrent sessions and don't want to own the orchestration.
  • You need persistent profiles without building a storage layer.
  • You want to watch a session live while it runs — useful for agent debugging, where a failed task is often a single misread element.
  • You want per-session proxy configuration without managing proxy credentials in your own infrastructure.

Run your own Chrome when you have a platform team, a compliance requirement that forbids third-party browser execution, or a workload so steady-state that the economics clearly favor reserved compute.

Remote Browser sits in the first category: hosted Chromium sessions with CDP access, Playwright and Puppeteer compatibility, persistent profiles, configurable browser settings, session isolation, and a live viewer. You connect with the same connectOverCDP call shown above. If you want to try it without writing code first, Remote Browser online walks through the browser-in-the-cloud path.

A note on stealth and detection

CDP itself is detectable. A page can check for navigator.webdriver, inspect CDP-injected properties, or measure timing anomalies. Connecting over CDP does not make you invisible, and no provider's claims about undetectability should be taken at face value.

What actually helps: a real Chrome build rather than a patched one, a consistent user agent and viewport, residential or mobile IPs that match the target geography, and human-like interaction timing. Hosted runtimes expose configurable browser settings for some of this, but treat any provider claiming guaranteed undetectability as a red flag. Detection is an arms race, not a checkbox.

Summary

chromium.connectOverCDP() is the entire API surface for connecting Playwright to remote Chrome. The complexity lives in the endpoint, the session lifecycle, and the operational properties of whatever is serving CDP on the other side.

Start with the code above against a local Chrome on port 9222 to confirm your script works. Then swap the endpoint for a hosted one and verify the six production criteria — stability, isolation, persistence, proxy, reaping, and version control — before you put it in front of real traffic. For the full API surface, see the documentation; for current usage-based pricing, see /pricing.