← Blog

BLOG

Playwright connectOverCDP Example With WebSocket Endpoint

A working Playwright connectOverCDP example using a WebSocket endpoint, plus production notes on CDP sessions, context handling, and remote Chromium.

October 6, 20268 min readRemote Browser

# Playwright connectOverCDP Example With WebSocket Endpoint

If you want a Playwright connectOverCDP example with a WebSocket endpoint, the short version is: you pass a ws:// or wss:// DevTools URL to chromium.connectOverCDP(), and Playwright attaches to the already-running browser instead of launching its own. That single call is the difference between a local test harness and a remote Chromium session your agent, CI job, or backend worker can drive from anywhere.

This post gives you a complete, runnable example, then covers the parts that bite people in production: how contexts and pages behave when you attach over CDP, how to keep a session alive across workers, and what to check before you point real traffic at a remote endpoint.

What connectOverCDP actually does

connectOverCDP is a method on BrowserType. In Playwright, chromium.connectOverCDP(endpointURL) opens a connection to a Chromium-based browser that is already running and exposes a DevTools Protocol endpoint. Playwright does not spawn a process. It speaks CDP to whatever is listening at that URL.

That distinction matters for three reasons:

  • You don't own the browser lifecycle. The remote runtime starts, isolates, and tears down the browser. Your code just connects.
  • The endpoint is a WebSocket. CDP exposes an HTTP discovery endpoint (/json/version) that returns a webSocketDebuggerUrl. That ws:// URL is what you hand to Playwright.
  • State can outlive your script. Because the browser isn't tied to your process, a session can persist across deploys, retries, or multiple workers.

The official Playwright CDP documentation is the authoritative reference for the method signature and supported options. Read it alongside this post; the example below is the practical wiring, not a replacement for the API docs.

The WebSocket endpoint, explained

When you launch Chrome with --remote-debugging-port=9222, it serves two things:

  1. http://localhost:9222/json/version — a JSON blob with browser metadata and a webSocketDebuggerUrl.
  2. The WebSocket itself, e.g. ws://localhost:9222/devtools/browser/<id>.

Playwright wants the WebSocket URL, not the HTTP one. If you pass the HTTP discovery URL, you'll get a connection error. A hosted runtime typically hands you the full wss:// URL directly, so you skip the discovery step entirely.

A remote endpoint looks like this:

wss://<host>/cdp/<session-id>?token=<short-lived-token>

The exact shape depends on the provider. What matters is that it's a browser-level CDP WebSocket, and that the token is scoped to a single session. Treat that URL as a secret — anyone holding it can drive the browser.

A complete Playwright connectOverCDP example

Here's a TypeScript example that connects to a remote Chromium over a WebSocket endpoint, reuses the existing context, navigates, and extracts data. It assumes PLAYWRIGHT_CDP_ENDPOINT is set in your environment.

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

const endpoint = process.env.PLAYWRIGHT_CDP_ENDPOINT;

if (!endpoint) {
  throw new Error('PLAYWRIGHT_CDP_ENDPOINT is not set');
}

async function run(): Promise<void> {
  // Attach to the already-running remote Chromium.
  const browser: Browser = await chromium.connectOverCDP(endpoint, {
    timeout: 30_000,
  });

  // A CDP-attached browser usually has one default context already.
  const contexts: BrowserContext[] = browser.contexts();
  const context: BrowserContext =
    contexts.length > 0 ? contexts[0] : await browser.newContext();

  // Reuse an existing page if present, otherwise open one.
  const pages: Page[] = context.pages();
  const page: Page = pages.length > 0 ? pages[0] : await context.newPage();

  try {
    await page.goto('https://example.com', {
      waitUntil: 'domcontentloaded',
      timeout: 45_000,
    });

    const title = await page.title();
    const heading = await page.locator('h1').first().innerText();

    console.log({ title, heading });
  } finally {
    // Close the connection, not the remote browser.
    await browser.close();
  }
}

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

Three details are easy to miss:

  • `browser.contexts()` is not empty. When you attach over CDP, the browser already has a default context. Calling newContext() blindly creates a second one and can confuse page reuse.
  • `browser.close()` disconnects. It does not kill the remote browser. That's usually what you want — the session stays alive for the next worker.
  • Set an explicit `timeout`. Remote endpoints add network latency. The default 30s is often fine, but a cold session or a slow proxy can exceed it.

Contexts, pages, and what CDP does not give you

This is where most connectOverCDP bugs come from. CDP attachment is not identical to a locally launched browser.

BehaviorLocal chromium.launch()chromium.connectOverCDP()
Browser processYou own itRemote runtime owns it
Default contextCreated on launchAlready exists
browser.close()Kills the browserDisconnects only
browser.newContext()Fully supportedWorks, but may not isolate as expected
Persistent profileManual userDataDirManaged by the runtime
Storage stateYou save/load itOften persists server-side
Tracing / videoFull supportDepends on runtime capabilities

The practical takeaway: prefer the existing context when you attach over CDP. If you need strict isolation between tasks, don't rely on newContext() inside a shared browser — request a fresh session from the runtime instead. Session isolation at the runtime layer is more reliable than context isolation inside one browser process.

If you're building agent workloads where each task needs its own profile, cookies, and proxy, that's a runtime concern, not a Playwright concern. The remote browser for AI agents post covers how session-per-task isolation is typically structured.

Getting the endpoint from a hosted runtime

With a hosted Chromium runtime, the flow is usually:

  1. Create a session via the runtime's API (REST or SDK).
  2. Receive a CDP WebSocket URL plus a session ID.
  3. Pass that URL to chromium.connectOverCDP().
  4. Run your automation.
  5. Release the session when done.

The endpoint is short-lived and scoped. Don't cache it across long periods — re-request a fresh one per task. If you're comparing this to running Chromium locally, the remote browser online guide walks through the trade-offs.

A minimal session-creation helper looks like this:

async function createSession(): Promise<string> {
  const res = await fetch(`${process.env.RUNTIME_API}/sessions`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.RUNTIME_TOKEN}`,
    },
    body: JSON.stringify({ profile: 'default' }),
  });

  if (!res.ok) {
    throw new Error(`Session create failed: ${res.status}`);
  }

  const data = (await res.json()) as { cdpUrl: string };
  return data.cdpUrl;
}

The exact request shape depends on the provider. Check the documentation for the current API surface rather than hardcoding assumptions.

Production criteria before you ship

A connectOverCDP example that works on your laptop is not the same as one that survives production. Evaluate these before you commit:

  • Reconnect behavior. Remote WebSockets drop. Your worker needs to detect a closed connection and either reconnect or fail the task cleanly. Don't assume the socket lives for the whole job.
  • Session lifetime. How long does a session stay alive between calls? If your agent pauses for a human approval step, does the browser survive? This is a runtime policy, not a Playwright setting.
  • Isolation model. One browser per task, or one browser shared across tasks with separate contexts? The first is safer for anything touching authenticated state.
  • Proxy and network settings. If your workload hits protected sites, the browser's egress IP matters more than your Playwright code. Configurable browser settings at the runtime layer handle this; you can't fix it from connectOverCDP.
  • Observability. Can you watch the session live? A viewer is the difference between debugging in five minutes and guessing for an hour. See remote control browser for how live session viewing fits into agent workflows.
  • Cost model. Browser time is usually metered. Know whether you're billed per session, per browser-hour, or per task. Current rates are on the pricing page.

Common errors and how to read them

ErrorLikely causeFix
connect ECONNREFUSEDWrong host/port, or browser not runningVerify the endpoint is reachable from your network
Unexpected server response: 404Passed the HTTP discovery URLUse the ws:// / wss:// URL
Target closed mid-runSession expired or was reapedRecreate the session; check lifetime policy
browser.newContext: Not supportedRuntime restricts context creationUse the existing context or request a new session
Timeouts on gotoSlow proxy or cold sessionRaise timeout; warm the session before the critical path

The Target closed error is the one that catches people. It usually means the remote session hit its idle or max-lifetime limit. Your retry logic should create a new session, not reconnect to the dead one.

When connectOverCDP is the right choice

Use connectOverCDP when:

  • You already have a browser running somewhere and want Playwright to drive it.
  • You need to attach to a session that a human or another process started.
  • You're integrating with a runtime that exposes CDP rather than a Playwright-native API.
  • You want to reuse an authenticated profile without re-logging in.

Don't use it when:

  • You need full Playwright feature parity (some features assume a launched browser).
  • You need strict per-task isolation and the runtime doesn't provide it.
  • You're on Firefox or WebKit — connectOverCDP is Chromium-only.

For most agent and automation workloads, the pattern is: runtime creates an isolated session, hands you a WebSocket endpoint, you connectOverCDP, do the work, and let the runtime clean up. That keeps your code portable and pushes the hard parts — isolation, proxies, lifecycle — into infrastructure that's built for them.

Putting it together

The connectOverCDP example above is the whole mechanism: one WebSocket URL in, a live remote Chromium out. The complexity isn't in the Playwright call — it's in everything around it. Session lifetime, isolation, reconnection, and observability are what separate a demo from something you can run on a schedule.

Start with the example, get it connecting to a hosted session, then harden the edges: explicit timeouts, reconnect logic, and a clear answer to "what happens when the socket drops mid-task." If you want to see how a managed runtime handles those edges for you, the remote web browser overview is a reasonable next read.