← Blog

BLOG

Playwright ConnectOverCDP Download: What You Actually Install

Playwright connectOverCDP download explained: what the command installs, how to connect to remote Chromium, and when to skip local browser downloads.

September 18, 20269 min readRemote Browser

# Playwright ConnectOverCDP Download: What You Actually Install

If you searched for "playwright connectovercdp download," you probably want one of two things: the code to connect Playwright to a remote Chromium instance over the Chrome DevTools Protocol, or the browser binaries that connectOverCDP seems to require. The honest answer is that connectOverCDP does not download a browser at all. It attaches to one that is already running and already exposing a CDP endpoint. The download you are looking for is either the Playwright package itself, or — if you are running against a hosted runtime — nothing, because the browser lives on the other side of a WebSocket URL.

This guide covers what each piece actually does, how to wire up a connection, and how to decide whether you should be downloading Chromium locally at all.

What connectOverCDP actually does

browserType.connectOverCDP(endpointURL) is a Playwright method that opens a CDP session to a running browser and returns a Browser object. It does not launch a process. It does not install anything. It speaks the DevTools Protocol over HTTP and WebSocket to whatever is listening at the endpoint you pass in.

That distinction matters because most "download" confusion comes from mixing up three separate things:

  • The Playwright library — installed via npm, pip, or another package manager. This is code.
  • Browser binaries — installed via npx playwright install or npx puppeteer browsers install chrome. These are the actual Chromium/Firefox/WebKit builds.
  • A running browser with a CDP endpoint — either a local Chrome you launched with --remote-debugging-port, or a remote browser session that hands you a ws:// URL.

connectOverCDP only needs the first and third. The second is optional, and if you are connecting to a hosted runtime, it is unnecessary.

Playwright's own documentation is explicit that connectOverCDP is Chromium-only. Firefox and WebKit do not expose a compatible CDP surface, so if your stack depends on cross-browser coverage, CDP attachment is not the path — you need Playwright's native remote server protocol instead. See the Playwright CDP documentation for the authoritative method signature.

The three "downloads" people actually mean

When someone types "playwright connectovercdp download" into a search bar, they are usually trying to resolve one of these:

What you think you needWhat it actually isCommand or source
Playwright itselfThe npm/pip packagenpm i -D playwright
Chromium for CDPLocal browser binarynpx playwright install chromium
A CDP endpointRunning browser with debug portchrome --remote-debugging-port=9222
A remote browserHosted session with a ws:// URLRuntime API or dashboard
Puppeteer's browserPuppeteer-managed Chromiumnpx puppeteer browsers install chrome

If you are connecting to a remote runtime, rows two and five are irrelevant. You never download a browser; you receive a WebSocket endpoint and pass it to connectOverCDP. That is the entire integration.

This is also why the related searches around puppeteer browsers install chrome and puppeteer browsers npm keep showing up alongside Playwright queries. Both libraries historically bundled browser downloads, and both now let you skip them when you point at an external endpoint. Puppeteer's browserWSEndpoint option and Playwright's connectOverCDP are solving the same problem from two different APIs.

Connecting Playwright to a remote Chromium over CDP

Here is the minimal working pattern. Assume your runtime gives you a WebSocket debugger URL.

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

async function connectToRemoteBrowser(wsEndpoint: string): Promise<void> {
  // connectOverCDP attaches to an already-running browser.
  // No local Chromium download is required.
  const browser: Browser = await chromium.connectOverCDP(wsEndpoint, {
    timeout: 30_000,
  });

  // A remote session usually has one default context already open.
  const contexts: BrowserContext[] = browser.contexts();
  const context: BrowserContext =
    contexts.length > 0 ? 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 session unless
  // you intend to tear the session down. Disconnect instead.
  await browser.close();
}

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

A few production notes that the docs do not emphasize enough:

  • `browser.close()` on a CDP connection disconnects the client. Whether it also kills the remote browser depends on the runtime. On a hosted session, you usually want to release the session through the runtime's own API rather than relying on Playwright's close semantics.
  • Contexts come pre-created. Unlike launch(), connectOverCDP does not give you a clean slate. You inherit whatever contexts and pages the remote browser already has. Check browser.contexts() before assuming a fresh state.
  • Timeouts are your friend. Remote endpoints can be slow to accept the WebSocket handshake. Set an explicit timeout and handle the rejection.
  • Version skew is real. CDP is a moving target. If your Playwright version is far ahead of the remote Chromium build, some protocol methods may not exist. Pin versions on both sides when you can.

When you still need to download a browser

There are legitimate cases where npx playwright install chromium is the right call:

  • Local development and debugging. Running Chromium on your machine gives you DevTools, breakpoints, and fast iteration.
  • CI pipelines with no remote runtime. If your tests run in a container and you have no hosted browser, you need the binary.
  • Offline or air-gapped environments. No endpoint, no CDP.
  • Reproducing a bug against a specific Chromium build. You control the exact version.

The trade-off is operational. Every environment that downloads a browser also owns the version, the system dependencies (playwright install-deps), the disk footprint, and the upgrade cadence. Multiply that across a fleet of workers and it becomes a maintenance surface rather than a convenience.

If you are running browser automation at any real volume — especially for AI agents that spin sessions up and down — the download step is often the thing you want to eliminate. That is the case for a hosted runtime: the browser is already running, already patched, and already reachable over CDP. You can read more about that model in Remote Browser for AI agents.

Remote browser vs local browser download: a decision table

CriterionLocal download (playwright install)Remote CDP endpoint
Setup time per environmentMinutes to tens of minutesOne env var
Version managementYou own itRuntime owns it
System dependenciesYou install themNone
Session isolationManual (containers, profiles)Per-session by default
Persistent profilesYou manage the directoryRuntime-managed
Proxy configurationYou pass launch argsConfigurable per session
ScalingBound by worker disk/CPUBound by runtime capacity
DebuggingFull local DevToolsLive viewer + CDP
Cost modelCompute you already pay forMetered per session

Neither column wins universally. The question is whether browser lifecycle is core to your product or incidental to it. For most agent workloads, it is incidental.

Puppeteer, browserWSEndpoint, and the same pattern

The Puppeteer equivalent is worth knowing because the search intent overlaps heavily. Puppeteer's puppeteer.connect({ browserWSEndpoint }) does exactly what connectOverCDP does — attaches to a running browser over CDP. The browserWSEndpoint you pass is the same kind of ws:// URL.

import puppeteer from 'puppeteer';

const browser = await puppeteer.connect({
  browserWSEndpoint: process.env.CDP_WS_ENDPOINT,
  defaultViewport: null,
});

If you have seen puppeteer browserwsendpoint example or puppeteer browserwsendpoint github in search results, this is the pattern they are describing. The endpoint is typically obtained from a runtime's session-creation API, not constructed by hand. Puppeteer's own puppeteer browsers install chrome command is only needed when you are launching locally.

The practical upshot: whether you use Playwright or Puppeteer, the remote path is the same shape. Get a WebSocket URL, connect, drive, disconnect. No browser download on the client.

Production criteria for a CDP endpoint

Not every ws:// URL is production-grade. When evaluating a remote browser runtime for CDP attachment, check these:

  1. Session isolation. Each session should have its own browser process or at least its own context boundary. Shared state across tenants is a correctness bug waiting to happen.
  2. Persistent profiles. If your agent needs to stay logged in across sessions, the runtime must support profile persistence. Otherwise every run starts cold.
  3. Proxy and network controls. Configurable browser settings for egress IP, region, and headers matter for anything touching geo-restricted or rate-limited sites.
  4. Live observability. A viewer or session recording is the difference between debugging in five minutes and debugging for an hour.
  5. Usage controls. You want per-session metering and hard limits, not a surprise bill. Current pricing and limits are on the pricing page.
  6. CDP fidelity. The endpoint should expose a real Chromium CDP surface, not a shim. If page.context().newCDPSession(page) fails, you have a problem.

The last point is worth testing explicitly. Some runtimes proxy CDP and drop methods. A quick smoke test:

const cdp = await page.context().newCDPSession(page);
await cdp.send('Network.enable');

If that throws, the endpoint is not a full CDP surface.

Common failure modes

"connectOverCDP hangs." Usually a network path issue — the WebSocket URL is reachable from the runtime's network but not yours, or a proxy is stripping the upgrade header. Test with wscat before blaming Playwright.

"Target closed" immediately after connect. The remote session expired or was reaped. Check session TTL and whether your runtime closes idle sessions.

"Protocol error: method not found." Version skew between Playwright and the remote Chromium. Pin both.

"Works locally, fails in CI." Your CI has no route to the endpoint, or the endpoint requires auth headers Playwright is not sending. Some runtimes embed credentials in the URL; others need a header.

"Browser downloads on every CI run." You are still calling playwright install in a pipeline that connects remotely. Remove the install step. If a dependency forces it, check whether you are accidentally calling chromium.launch() somewhere.

Where Remote Browser fits

Remote Browser provides hosted Chromium sessions with CDP access, so connectOverCDP works without any local browser download. Sessions are isolated, profiles can persist, browser settings are configurable, and there is a live viewer for debugging. Playwright, Puppeteer, and Selenium clients all connect over the same CDP surface.

The integration is one environment variable. You create a session, receive a WebSocket endpoint, and pass it to connectOverCDP. No playwright install, no install-deps, no version matrix.

If you want the broader picture of how hosted sessions differ from local setup, Remote Browser online covers the runtime model, and the documentation has the connection details for each client library.

Summary

connectOverCDP does not download anything. It attaches to a running browser over the DevTools Protocol. The download you were probably looking for is either the Playwright package (npm i playwright) or a local Chromium binary (npx playwright install chromium) — and the second is only necessary if you are launching browsers yourself.

If you are connecting to a remote runtime, skip the browser download entirely. Get a ws:// endpoint, call chromium.connectOverCDP(endpoint), and handle the session lifecycle through the runtime's API rather than Playwright's close(). Test that the endpoint exposes a real CDP surface, pin your Playwright version, and set explicit timeouts.

The decision between local and remote comes down to whether browser lifecycle is your problem or someone else's. For most agent and automation workloads, it is better as someone else's.