← Blog

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.

October 5, 202610 min readRemote Browser

# 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/version returns metadata including a webSocketDebuggerUrl.
  • That webSocketDebuggerUrl (typically ws://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:

  1. 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.
  2. The socket is a single point of failure. If it drops, the Playwright Browser object 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-run

Key flags and why they matter:

  • --remote-debugging-port=9222 opens the CDP listener.
  • --remote-debugging-address=0.0.0.0 binds 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-dir gives the process a writable profile directory. Without it, Chrome may refuse to start a second instance or share state with an existing one.
  • --headless=new runs 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/version

You 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

SymptomLikely causeFix
connect ECONNREFUSEDChrome not listening, or bound to loopback onlyAdd --remote-debugging-address=0.0.0.0; check firewall
connect ETIMEDOUTNetwork path blockedVerify security groups, VPN, or tunnel
WebSocket error: 403Endpoint requires auth headersPass headers in connectOverCDP options or use a tokenized URL
Connects, then Target closedRemote browser crashed or was reapedAdd health checks; use a runtime with session lifecycle management
Works once, fails on second runStale --user-data-dir lockUse a unique profile dir per session
Slow actions, no errorsHigh RTT to browser hostCo-locate client and browser in the same region
browser.contexts() empty unexpectedlyConnected to a fresh browser, not the one you meantConfirm 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.

DimensionSelf-hosted Chrome + CDPHosted runtime (e.g. Remote Browser)
SetupInstall Chrome, manage flags, open portsGet an endpoint, connect
ScalingYou build scheduling and poolingSessions provisioned via API
IsolationYour responsibility (containers, users)Session-level isolation built in
Profile persistenceManual --user-data-dir managementPersistent profiles as a feature
Proxies / network settingsConfigure per processConfigurable browser settings
Live debuggingVNC or screenshots you wire upLive viewer
Auth on the endpointYou build itToken-based access
Cost modelCompute + your engineering timeUsage-based; see /pricing
Best forFull control, custom Chrome buildsTeams 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. connectOverCDP covers 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:

  1. Request a session from the runtime API.
  2. Receive a ws:// or wss:// endpoint plus any auth token.
  3. Pass it to chromium.connectOverCDP() exactly as in the code above.
  4. 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.