cdpfleet Docs GitHub Dashboard

Cases / #42 · 2026-10-13 · Easy

Puppeteer on real Google Chrome: cdpUrl, raw CDP and request interception

Drive real Chrome on cdpfleet with Puppeteer (or any CDP client): what the browser reports, raw CDP calls, blocking images, and closing the session.

Google Chrome

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

The problem

A lot of browser automation is written for Puppeteer or speaks the Chrome DevTools Protocol directly. With "cdp": true, a cdpfleet launch returns a cdpUrl for exactly those clients — but is the browser on the other end really Google Chrome, can you send raw CDP commands, does Puppeteer's request interception work through it, and what happens to the session when the client closes? Case 26 compared Playwright's own protocol with CDP; this one is about using CDP the way Puppeteer users do.

What we used, and why

WhatWhy
chrome with "cdp": trueReal Google Chrome, headless; the launch response carries cdpUrl next to wsUrl. See CDP & Puppeteer.
Node: puppeteer.connect({ browserWSEndpoint: cdpUrl, headers })Puppeteer (puppeteer-core 24, no browser download). Its version doesn't have to match anything on our side.
Python, Java, C#, Go: connectOverCDP(cdpUrl)These languages have no maintained Puppeteer, so they use Playwright's CDP client plus raw CDP sessions (newBrowserCDPSession, newCDPSession) for the same commands. Go's chromedp and Rod connect to cdpUrl too.
Browser.getVersion, Target.getTargetsRaw CDP at the browser level: the product string, the protocol version and the page targets.
Request interceptionPuppeteer's setRequestInterception / Playwright's route: abort every image request on books.toscrape.com and compare the bytes the page pulled, from its own resource timing, each load in a fresh context.
Performance.getMetricsPage metrics over raw CDP — Puppeteer wraps it as page.metrics().
GET /v1/me/threadsAfter the client closes, how long until the session is gone from the live list.

How it works

  1. Launch Chrome with "cdp": true and connect to cdpUrl.
  2. Ask the browser for its version and count its page targets over raw CDP.
  3. Load books.toscrape.com in a fresh context; load it again in another fresh context with images aborted; count targets again.
  4. Read the first page's performance metrics over CDP, then close the client and watch the session disappear.

The code

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

// npm install puppeteer-core@24
// env: CDPFLEET_API_KEY, PROXY_URL
// Puppeteer on real Google Chrome: launch with "cdp": true and connect to cdpUrl. No browser
// download is needed (puppeteer-core), and the Puppeteer version doesn't have to match anything.
import puppeteer from 'puppeteer-core';

const KEY = process.env.CDPFLEET_API_KEY;
const URL = 'https://books.toscrape.com/';

// Bytes the page pulled over the network, from its own resource timing (fresh context: no cache).
const TRANSFER = `(() => {
  const all = [...performance.getEntriesByType('navigation'), ...performance.getEntriesByType('resource')];
  return { resources: all.length, transfer_kb: Math.round(all.reduce((n, e) => n + (e.transferSize || 0), 0) / 1024) };
})()`;

async function launch() {
  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: true, record: false }),
  });
  if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
  return res.json();
}

async function isLive(sessionId) {
  const res = await fetch('https://cdpfleet.com/v1/me/threads', { headers: { 'x-api-key': KEY } });
  return (await res.text()).includes(sessionId);
}

async function goto(page) {
  for (let attempt = 1; ; attempt++) { // the proxy can drop a tunnel; retry
    try { await page.goto(URL, { waitUntil: 'load', timeout: 60000 }); return; } catch (err) { if (attempt === 3) throw err; }
  }
}

// One page in a fresh browser context, optionally aborting image requests.
async function load(browser, blockImages) {
  const context = await browser.createBrowserContext();
  const page = await context.newPage();
  let blocked = 0;
  if (blockImages) {
    await page.setRequestInterception(true);
    page.on('request', (req) => {
      if (req.resourceType() === 'image') { blocked++; req.abort(); } else req.continue();
    });
  }
  const t = Date.now();
  await goto(page);
  const loadMs = Date.now() - t;
  const transfer = await page.evaluate(TRANSFER);
  return { context, page, row: { label: blockImages ? 'images blocked' : 'everything', ...transfer, images_blocked: blocked, load_ms: loadMs } };
}

const s = await launch();
const t = Date.now();
const browser = await puppeteer.connect({ browserWSEndpoint: s.cdpUrl, headers: { 'x-api-key': KEY }, defaultViewport: null });
const connectMs = Date.now() - t;
const out = { client: 'Puppeteer (puppeteer-core 24)', connect_ms: connectMs };
try {
  // Raw CDP at the browser level: what's really on the other end.
  const cdp = await browser.target().createCDPSession();
  const version = await cdp.send('Browser.getVersion');
  out.product = version.product;
  out.protocol_version = version.protocolVersion;
  const pages = async () => (await cdp.send('Target.getTargets')).targetInfos.filter((x) => x.type === 'page').length;
  const before = await pages();

  const full = await load(browser, false);
  const lean = await load(browser, true);
  out.targets = { pages_before: before, pages_after: await pages() };
  out.loads = [full.row, lean.row];

  // Puppeteer wraps Performance.getMetrics as page.metrics().
  const m = await full.page.metrics();
  out.metrics = { dom_nodes: m.Nodes, js_heap_used_mb: Math.round(m.JSHeapUsedSize / 1048576 * 10) / 10 };
  await full.context.close();
  await lean.context.close();
} finally {
  await browser.close(); // closes the connection, which ends the cdpfleet session
}
const closed = Date.now();
while (await isLive(s.sessionId)) {
  if (Date.now() - closed > 60000) break;
  await new Promise((r) => setTimeout(r, 1000));
}
out.session_ended_s = (await isLive(s.sessionId)) ? null : Math.round((Date.now() - closed) / 100) / 10;
console.log(JSON.stringify(out, null, 2));

What we got

MeasuredValue
ClientPuppeteer (puppeteer-core 24)
Connect202 ms
Browser.getVersionChrome/155.0.8059.39 (protocol 1.3)
Page targets0 → 2
Load: everything331 KB in 4280 ms
Load: images blocked152 KB in 1561 ms, 32 images aborted
Performance.getMetrics1640 DOM nodes, 1.2 MB JS heap
Session gone after close0.1 s

From the Node.js run on 2026-10-13. 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)
{
  "client": "Puppeteer (puppeteer-core 24)",
  "connect_ms": 202,
  "product": "Chrome/155.0.8059.39",
  "protocol_version": "1.3",
  "targets": {
    "pages_before": 0,
    "pages_after": 2
  },
  "loads": [
    {
      "label": "everything",
      "resources": 32,
      "transfer_kb": 331,
      "images_blocked": 0,
      "load_ms": 4280
    },
    {
      "label": "images blocked",
      "resources": 44,
      "transfer_kb": 152,
      "images_blocked": 32,
      "load_ms": 1561
    }
  ],
  "metrics": {
    "dom_nodes": 1640,
    "js_heap_used_mb": 1.2
  },
  "session_ended_s": 0.1
}

Takeaways