BLOG
Playwright Connect To Remote Chrome Over WebSocket
Learn how to use Playwright connect to remote Chrome over WebSocket with CDP, plus production trade-offs, code, and hosted runtime options.
# Playwright Connect To Remote Chrome Over WebSocket
To connect Playwright to a remote Chrome instance over WebSocket, you point chromium.connectOverCDP() at a ws:// or wss:// DevTools endpoint exposed by that Chrome process. Playwright speaks the Chrome DevTools Protocol (CDP) over that socket, so your script drives a browser running on another machine, container, or hosted runtime instead of launching a local binary. This guide covers the exact connection code, the flags that make a remote Chrome reachable, the failure modes you will hit, and how to decide between self-hosting and a managed runtime.
If you are running browser automation at any real volume, the connection step is rarely the hard part. Keeping the endpoint alive, isolated, and reachable from your workers is. That is the problem a hosted runtime like Remote Browser is built to absorb, and it is worth understanding the mechanics before you commit to either path.
What "connect over WebSocket" actually means
Chrome exposes a DevTools endpoint when it starts with --remote-debugging-port. Two URLs matter:
http://host:9222/json/versionreturns metadata including awebSocketDebuggerUrl.- That
webSocketDebuggerUrl(typicallyws://host:9222/devtools/browser/<id>) is the browser-level CDP socket.
Playwright's connectOverCDP() takes either the HTTP endpoint or the WebSocket URL and negotiates the rest. Under the hood it opens the browser-level socket, then creates a CDP session per page or target. Every page.click(), page.goto(), and network interception call becomes a CDP message on that socket.
Two consequences follow from this:
- Latency is now network latency. A local
launch()talks over a pipe. A remote connection talks over TCP, then WebSocket framing. Round-trip time to the browser host becomes part of every action. - The socket is a single point of failure. If it drops, the Playwright
Browserobject goes stale and every subsequent call throws. Reconnection is your responsibility.
Playwright documents this API in the BrowserType.connectOverCDP reference. Note that connectOverCDP is Chromium-only; Firefox and WebKit do not implement CDP, so this path is Chrome, Chromium, Edge, and other Chromium derivatives.
Starting a remote Chrome that accepts WebSocket connections
On the machine that will host the browser, launch Chrome with remote debugging enabled and bound to an address your client can reach:
chrome --headless=new \
--remote-debugging-port=9222 \
--remote-debugging-address=0.0.0.0 \
--user-data-dir=/tmp/chrome-profile \
--no-first-runKey flags and why they matter:
--remote-debugging-port=9222opens the CDP listener.--remote-debugging-address=0.0.0.0binds to all interfaces. The default binds to loopback only, which is the single most common reason a connection "works locally but not remotely."--user-data-dirgives the process a writable profile directory. Without it, Chrome may refuse to start a second instance or share state with an existing one.--headless=newruns the modern headless mode. You can omit it if you need a real display, but then you also need a display server.
Then verify the endpoint before touching Playwright:
curl http://browser-host:9222/json/versionYou should get JSON back containing webSocketDebuggerUrl. If curl fails, Playwright will fail too, and the error will be less informative. Always test the raw endpoint first.
Security note: an open CDP port is full remote control of that browser, including cookies and authenticated sessions. Never expose port 9222 to the public internet without a tunnel, an authenticating proxy, or network-level access control. This is the reason most teams end up using a managed endpoint with auth built in rather than a bare port.
The Playwright connection code
Here is a minimal but production-shaped TypeScript example. It connects over CDP, reuses the existing browser context, and handles the case where the remote browser already has pages open.
import { chromium, Browser, BrowserContext, Page } from 'playwright';
const CDP_ENDPOINT = process.env.CDP_ENDPOINT ?? 'ws://browser-host:9222';
async function connect(): Promise<{ browser: Browser; context: BrowserContext; page: Page }> {
const browser = await chromium.connectOverCDP(CDP_ENDPOINT, {
timeout: 30_000,
});
// A remote browser may already have contexts/pages. Reuse the first one
// rather than assuming a clean slate.
const context = browser.contexts()[0] ?? (await browser.newContext());
const page = context.pages()[0] ?? (await context.newPage());
return { browser, context, page };
}
async function main() {
const { browser, page } = await connect();
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const title = await page.title();
console.log('Remote page title:', title);
// Raw CDP is available when Playwright's API does not cover a case.
const cdp = await page.context().newCDPSession(page);
const { result } = await cdp.send('Runtime.evaluate', {
expression: 'navigator.userAgent',
returnByValue: true,
});
console.log('User agent via CDP:', result.value);
} finally {
// Do NOT call browser.close() on a shared remote browser unless you own it.
await browser.close();
}
}
main().catch((err) => {
console.error('Connection failed:', err);
process.exit(1);
});Three details that separate a demo from something that survives production:
- `browser.contexts()[0]` reuse. A freshly launched local browser has zero contexts. A remote browser that has been running for a while may have several. Assuming a clean state is a common source of flaky tests.
- `newCDPSession`. Playwright's high-level API covers most needs, but some CDP domains (certain performance metrics, some permission overrides) are only reachable through a raw session. Mixing the two is normal.
- `browser.close()` semantics. Over CDP,
close()disconnects Playwright. Whether it also terminates the remote browser depends on the endpoint. With a shared or hosted browser, you usually want to disconnect, not kill.
Common failure modes and what they mean
| Symptom | Likely cause | Fix |
|---|---|---|
connect ECONNREFUSED | Chrome not listening, or bound to loopback only | Add --remote-debugging-address=0.0.0.0; check firewall |
connect ETIMEDOUT | Network path blocked | Verify security groups, VPN, or tunnel |
WebSocket error: 403 | Endpoint requires auth headers | Pass headers in connectOverCDP options or use a tokenized URL |
Connects, then Target closed | Remote browser crashed or was reaped | Add health checks; use a runtime with session lifecycle management |
| Works once, fails on second run | Stale --user-data-dir lock | Use a unique profile dir per session |
| Slow actions, no errors | High RTT to browser host | Co-locate client and browser in the same region |
browser.contexts() empty unexpectedly | Connected to a fresh browser, not the one you meant | Confirm the endpoint maps to the intended instance |
The Target closed case deserves emphasis. A bare Chrome process has no supervisor. If it OOMs, gets killed by the container runtime, or the host reboots, your Playwright client sees a dead socket and no automatic recovery. Production setups need either a process supervisor plus reconnect logic, or a runtime that handles it for you.
Self-hosted remote Chrome vs a hosted runtime
Once you move past a single browser on a single box, the operational surface grows quickly: session isolation, profile persistence, proxy configuration, concurrency limits, and observability. Here is the honest comparison.
| Dimension | Self-hosted Chrome + CDP | Hosted runtime (e.g. Remote Browser) |
|---|---|---|
| Setup | Install Chrome, manage flags, open ports | Get an endpoint, connect |
| Scaling | You build scheduling and pooling | Sessions provisioned via API |
| Isolation | Your responsibility (containers, users) | Session-level isolation built in |
| Profile persistence | Manual --user-data-dir management | Persistent profiles as a feature |
| Proxies / network settings | Configure per process | Configurable browser settings |
| Live debugging | VNC or screenshots you wire up | Live viewer |
| Auth on the endpoint | You build it | Token-based access |
| Cost model | Compute + your engineering time | Usage-based; see /pricing |
| Best for | Full control, custom Chrome builds | Teams that want to ship agents, not infra |
The self-hosted path is not wrong. If you need a custom Chromium build, kernel-level isolation, or you already run a container platform with spare capacity, running Chrome yourself is reasonable. The cost is engineering time: reconnect logic, session cleanup, profile locking, and monitoring are all yours to build and maintain.
The hosted path trades some control for removing that entire category of work. If your actual product is an AI agent or an automation workflow, the browser runtime is a dependency, not a differentiator. That is the argument in Remote Browser for AI agents, and it applies whether you are running Playwright directly or through a framework.
Production criteria before you commit
Whichever path you choose, evaluate against these:
- Reconnect behavior. What happens when the socket drops mid-task? Does your code retry, or does the task fail?
- Session lifecycle. How are sessions created, reused, and torn down? Are stale sessions reaped automatically?
- Isolation. Can two concurrent tasks see each other's cookies or storage? They should not.
- Profile handling. Do you need logged-in state to persist across sessions? If so, how is that state stored and scoped?
- Network configuration. Do you need specific egress IPs, geolocation, or proxy routing? Confirm the runtime supports it before you build around it.
- Observability. When a task fails at 3 a.m., can you see what the browser saw? A live viewer or session recording changes debugging from guesswork to inspection.
- Cost shape. Per-session-hour pricing behaves very differently from per-task pricing when tasks are long-running. Check current details at /pricing rather than assuming.
For a broader look at what a hosted browser session includes, Remote Browser online covers the runtime model, and remote web browser covers the practical differences from a local install.
When to use WebSocket CDP and when not to
Use connectOverCDP over WebSocket when:
- The browser must run somewhere other than your client process (cloud, container, another region).
- You need to attach to an already-running browser with existing state.
- You want raw CDP access alongside Playwright's API.
- You are integrating with a runtime that exposes a CDP endpoint.
Do not use it when:
- You need Firefox or WebKit. CDP is Chromium-only; use Playwright's native
connect()with a Playwright server for cross-browser. - You need Playwright's full feature set. Some Playwright features assume a browser it launched itself.
connectOverCDPcovers the common cases but not every edge. - You only need a single local browser.
chromium.launch()is simpler and faster.
A useful mental model: connectOverCDP is the lowest-common-denominator control channel. It is powerful and widely supported, but it is a protocol, not a product. Everything above the protocol, session management, isolation, profiles, viewing, is what you either build or buy.
Getting a WebSocket endpoint without running Chrome yourself
If you want to skip the process management, a hosted runtime gives you a CDP WebSocket URL you can drop straight into connectOverCDP. The flow is:
- Request a session from the runtime API.
- Receive a
ws://orwss://endpoint plus any auth token. - Pass it to
chromium.connectOverCDP()exactly as in the code above. - Run your task; the runtime handles isolation, profiles, and teardown.
This keeps your Playwright code portable. The same script that connects to ws://localhost:9222 connects to a hosted endpoint by changing one environment variable. That portability is the main reason to keep the connection layer thin and avoid baking runtime-specific assumptions into your automation code.
For teams running agents rather than test suites, the same endpoint works with Puppeteer's browserWSEndpoint and Selenium's CDP bridge, so the runtime choice does not lock you into one client library.
Summary
Connecting Playwright to remote Chrome over WebSocket is a two-line operation: start Chrome with --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222, then call chromium.connectOverCDP() with the resulting endpoint. The complexity is not in the connection; it is in everything around it, session lifecycle, isolation, reconnection, and observability. Self-hosting gives you control and hands you the operational burden. A hosted runtime removes the burden and gives you an endpoint. Pick based on which of those you would rather own, and verify the current cost and limits at /pricing before you commit.