cdpfleet Docs GitHub Dashboard

Cases / #26 · 2026-10-07 · Medium

Same Chrome, two doors: Playwright's protocol (wsUrl) or CDP (cdpUrl)

One Chrome launched twice — driven once through Playwright's own protocol and once over the Chrome DevTools Protocol — and what changes for your code and for the page.

Google Chrome

Run on production on 2026-10-07: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

cdpfleet sessions can now be driven two ways. wsUrl speaks Playwright's own protocol: you need a Playwright client of the server's exact minor version, and you get everything Playwright offers. With "cdp": true you also get cdpUrl, a plain Chrome DevTools Protocol endpoint: Puppeteer, connectOverCDP in any Playwright version, chromedp, Rod and most agent frameworks connect there. Which should you use — is one faster, does one lose features, and does one give the automation away to the page?

What we used, and why

WhatWhy
chrome with "cdp": trueReal Google Chrome; the launch response then carries cdpUrl next to wsUrl. See CDP & Puppeteer.
chromium.connect(wsUrl)Playwright's own protocol. The client must match the server's minor version (1.60).
chromium.connectOverCDP(cdpUrl)Playwright on top of plain CDP — any recent client version works, in every language.
page.on("console"), page.pdf()Two features that depend on what the connection carries: console events and Chrome's PDF printer.
The stack-getter probeA page logs an error whose stack getter records whether a debugger read it — the classic tell of a CDP client that has Runtime.enabled the page.

How it works

  1. Launch Chrome with "cdp": true and connect with connect(wsUrl); open example.com, listen for console messages, run the probe, try page.pdf(), close.
  2. Launch again and do the same through connectOverCDP(cdpUrl).
  3. Print one row per connection: connect time, browser version, console events, PDF, what the probe saw, navigator.webdriver.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

// What a page can notice about the client driving it. A debugger that has Runtime.enable'd
// the page serializes logged errors, which reads their `stack` getter.
const PROBE = `(async () => {
  let stackRead = false;
  const e = new Error('probe');
  Object.defineProperty(e, 'stack', { get() { stackRead = true; return ''; } });
  console.debug(e);
  await new Promise((r) => setTimeout(r, 100));
  return { stack_read_by_debugger: stackRead, webdriver: navigator.webdriver };
})()`;

async function launch(cdp) {
  const res = await fetch('https://starter.cdpfleet.com/chrome/session', {
    method: 'POST',
    headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
    body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: 'new', cdp }),
  });
  if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
  return res.json();
}

async function measure(mode) {
  const s = await launch(mode !== 'playwright protocol');
  const t = Date.now();
  const browser = mode === 'playwright protocol'
    ? await chromium.connect(s.wsUrl, { headers: { 'x-api-key': KEY } })
    : await chromium.connectOverCDP(s.cdpUrl, { headers: { 'x-api-key': KEY } });
  const connect_ms = Date.now() - t;
  try {
    const page = await browser.newPage();
    const logged = [];
    page.on('console', (m) => logged.push(m.type()));
    for (let attempt = 1; ; attempt++) { // the proxy can drop a tunnel; retry
      try { await page.goto('https://example.com/', { timeout: 60000 }); break; } catch (err) { if (attempt === 3) throw err; }
    }
    const seen = await page.evaluate(PROBE);
    let pdf = false;
    try { pdf = (await page.pdf()).length > 0; } catch { /* not over this connection */ }
    return {
      mode,
      endpoint: mode === 'playwright protocol' ? 'wsUrl' : 'cdpUrl',
      client_version_must_match: mode === 'playwright protocol',
      connect_ms,
      browser_version: browser.version(),
      console_events: logged.includes('debug'),
      pdf,
      ...seen,
    };
  } finally {
    await browser.close();
  }
}

console.log(JSON.stringify([await measure('playwright protocol'), await measure('connectOverCDP')], null, 2));

What we got

ConnectionEndpointClient version must matchConnect (ms)BrowserConsole eventsPDFStack read by debuggernavigator.webdriver
playwright protocolwsUrlyes32155.0.8059.39yesyesnono
connectOverCDPcdpUrlno52155.0.8059.39yesyesnono

From the Node.js run on 2026-10-07. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)
[
  {
    "mode": "playwright protocol",
    "endpoint": "wsUrl",
    "client_version_must_match": true,
    "connect_ms": 32,
    "browser_version": "155.0.8059.39",
    "console_events": true,
    "pdf": true,
    "stack_read_by_debugger": false,
    "webdriver": false
  },
  {
    "mode": "connectOverCDP",
    "endpoint": "cdpUrl",
    "client_version_must_match": false,
    "connect_ms": 52,
    "browser_version": "155.0.8059.39",
    "console_events": true,
    "pdf": true,
    "stack_read_by_debugger": false,
    "webdriver": false
  }
]

Takeaways