BLOG
Playwright Connect To Remote Chrome Over CDP
Learn how to use Playwright connect to remote Chrome over CDP, including endpoint formats, session handling, and production trade-offs.
# Playwright Connect To Remote Chrome Over CDP
To connect Playwright to a remote Chrome instance over CDP, you call chromium.connectOverCDP() with a WebSocket or HTTP endpoint that exposes the Chrome DevTools Protocol. The remote process must be launched with --remote-debugging-port (or an equivalent flag) and reachable from your machine. Playwright then attaches to the existing browser context instead of launching a local one. This is the same mechanism used by hosted browser runtimes, CI harnesses, and AI agents that need a persistent, isolated Chrome session.
This guide covers the endpoint formats, the exact TypeScript call, how contexts and pages behave after you attach, and the production criteria that decide whether you self-host Chrome or point Playwright at a hosted runtime.
What "connect over CDP" actually does
The Chrome DevTools Protocol is a JSON-over-WebSocket protocol that exposes browser internals: targets, pages, network events, DOM, and input. Playwright's connectOverCDP opens a CDP session to an already-running browser and wraps it in Playwright's API surface.
Two things follow from that:
- You do not launch the browser. Playwright attaches. The browser's lifecycle, profile, and flags are controlled by whoever started it.
- You inherit existing state. Tabs that are already open, cookies in the profile, and the browser context are visible to your Playwright code.
This differs from chromium.launch(), where Playwright spawns a fresh process with a clean profile and its own flags. If you want a clean slate, you either launch locally or ask your remote runtime for a fresh session.
The protocol itself is documented at the Chrome DevTools Protocol repository, and Playwright's browser connection semantics are covered in the Playwright docs.
Endpoint formats you will encounter
A remote Chrome exposes CDP on one of two URL shapes:
| Format | Example | Notes |
|---|---|---|
| HTTP discovery | http://host:9222 | Playwright fetches /json/version to find the WebSocket URL |
| WebSocket | ws://host:9222/devtools/browser/<id> | Direct connection, no discovery step |
Hosted runtimes typically hand you a WebSocket URL with a token in the path or query string, because the browser is not on your network and the endpoint must be authenticated. Self-hosted Chrome behind a reverse proxy usually gives you the HTTP form.
Both work with connectOverCDP. The WebSocket form is more common in production because it skips the discovery round trip and lets the provider scope access per session.
The TypeScript call
Here is the minimal, correct pattern. Note that connectOverCDP returns a Browser, and you should reuse browser.contexts()[0] rather than creating a new context.
import { chromium, Browser, BrowserContext, Page } from 'playwright';
async function connectToRemoteChrome(cdpUrl: string): Promise<void> {
const browser: Browser = await chromium.connectOverCDP(cdpUrl, {
timeout: 30_000,
});
// A remote browser usually already has one default context.
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' });
console.log('Title:', await page.title());
// Do NOT call browser.close() on a shared remote browser unless you own it.
await browser.close();
}
connectToRemoteChrome(process.env.CDP_URL!).catch((err) => {
console.error('CDP connection failed:', err);
process.exit(1);
});Three details matter in production:
- `timeout` should be set explicitly. Default timeouts are tuned for localhost, not for a network hop to a remote host.
- `browser.contexts()[0]` is the correct entry point. Calling
newContext()on a connected browser creates an additional context, which is sometimes what you want but often is not. - `browser.close()` on a connected browser disconnects the CDP session. Whether it also terminates the remote browser depends on the runtime. On a hosted session, closing usually ends the session and you lose the profile state.
Contexts, pages, and what you inherit
After connectOverCDP resolves, the object graph looks like this:
browser.contexts()returns the contexts that already exist. A Chrome started with a user data dir typically has one.context.pages()returns the tabs currently open in that context.- New pages you create with
context.newPage()are real tabs in the remote browser and are visible in any live viewer the runtime provides.
This is why CDP attachment is useful for agent workflows: the agent can pick up a session where a previous step left off, with cookies and login state intact. It is also why you should not assume a clean environment. If your automation depends on a fresh profile, request a new session from the runtime rather than reusing an existing endpoint.
For a broader look at how hosted sessions are structured, see Remote Browser for AI agents.
Self-hosted Chrome vs a hosted CDP endpoint
The decision is not about whether CDP works. It is about who operates the browser.
| Criterion | Self-hosted Chrome | Hosted CDP runtime |
|---|---|---|
| Setup | Install Chrome, manage flags, expose port | Paste a connection URL |
| Isolation | You build it (containers, user data dirs) | Per-session by default |
| Scaling | You manage a pool and a scheduler | Runtime handles session allocation |
| Profile persistence | Manual user data dir management | Configurable persistent profiles |
| Network egress | Your IPs, your proxy config | Configurable proxy and browser settings |
| Debugging | SSH, port forwarding, local viewer | Live viewer in the browser |
| Failure modes | Port conflicts, zombie processes, disk | Endpoint expiry, session limits |
Self-hosting is reasonable when the browser must live inside your VPC, when you already run a container scheduler, and when you have someone who owns the Chrome fleet. It becomes expensive when you need many concurrent sessions, persistent profiles, and per-session network configuration, because those are the parts that turn a script into infrastructure.
A hosted runtime is the better fit when the browser is a dependency of an agent rather than the product. You get a CDP URL, connect Playwright, and the runtime handles isolation, profiles, and cleanup. Current usage details are on the pricing page.
Production criteria before you commit
Before you point a production agent at a CDP endpoint, check these:
- Endpoint lifetime. Does the URL expire? What happens to an in-flight session when it does?
- Session isolation. Are two concurrent sessions guaranteed separate profiles and cookies?
- Profile persistence. Can you resume a logged-in session across runs, and is that state encrypted at rest?
- Network controls. Can you set proxies per session, and are browser settings configurable for the sites you target?
- Observability. Is there a live viewer, a session log, or a replay you can inspect after a failure?
- Concurrency limits. What is the ceiling, and how is it enforced? Do not assume there is no cap on concurrent sessions.
- Failure semantics. If the remote browser crashes, does
connectOverCDPthrow, hang, or reconnect?
These are the questions that separate a demo from a runtime. They also determine how much of your code is automation logic versus retry and cleanup logic.
Common failure modes and how to read them
Most CDP connection problems fall into a small set of categories:
- `connect ECONNREFUSED` — the port is not open, the host is wrong, or the browser was started without
--remote-debugging-port. - `401` or `403` on the WebSocket upgrade — the token is missing, expired, or scoped to a different session.
- Connection succeeds, then hangs on `page.goto` — the browser is up but has no network egress, or a proxy is misconfigured.
- `Target closed` mid-run — the remote browser or session was terminated, often by a timeout or a quota.
- Duplicate contexts — you called
newContext()when you should have reusedcontexts()[0].
If you are seeing intermittent failures, the issue is usually session lifecycle rather than the CDP call itself. A guide to the runtime side of this is in Remote Browser online.
When to use CDP attachment vs other Playwright connection modes
Playwright offers more than one way to reach a remote browser, and picking the wrong one creates avoidable work:
- `connectOverCDP` — attach to an existing Chrome or Chromium over CDP. Best when the browser is already running and you want its state.
- `connect` — connect to a Playwright server started with
playwright run-server. Best when the remote side is Playwright-native and you want full protocol support. - `launch` with `channel` or `executablePath` — run locally. Best for development and for tests that must not depend on a network hop.
If your remote endpoint speaks CDP and nothing else, connectOverCDP is the only option. If the provider offers a Playwright server, connect gives you a cleaner abstraction. Many hosted runtimes support both; check the documentation before assuming.
For a comparison of control surfaces, see Remote control browser.
A note on Firefox and WebKit
connectOverCDP is a Chromium-only API. Firefox and WebKit do not implement CDP in a way Playwright can attach to for this purpose. If your test matrix requires Firefox or WebKit, you need a Playwright server (connect) or local launches. This is a hard constraint, not a configuration issue.
Practical checklist for a first connection
- Confirm the remote endpoint responds:
curl http://host:9222/json/versionfor HTTP, or a WebSocket client forws://. - Set an explicit
timeoutinconnectOverCDP. - Reuse
browser.contexts()[0]unless you deliberately want a new context. - Log the browser version from
browser.version()to confirm you are talking to the expected build. - Decide whether
browser.close()should end the session or just disconnect, and document it. - Add a retry with backoff around the connect call, not around the whole workflow.
- Verify session isolation by running two connections in parallel and checking cookies do not leak.
That last step catches more production bugs than any other single check.
Where Remote Browser fits
Remote Browser provides hosted Chromium sessions with CDP access, so connectOverCDP works without you running a Chrome fleet. Sessions are isolated, profiles can persist across runs, browser and network settings are configurable, and a live viewer lets you watch or debug a session while it runs. Playwright, Puppeteer, and Selenium clients all connect to the same endpoint.
If you are moving from a local Chrome to a remote one, the code change is one line: replace chromium.launch() with chromium.connectOverCDP(url). The rest of your Playwright code stays the same. The operational change is larger, and that is the point: session lifecycle, isolation, and cleanup move out of your process.
Start with the documentation for endpoint formats and session options, and check pricing for current usage details. For a wider view of how remote browsers fit into agent stacks, see Remote web browser.