cdpfleet Docs GitHub Dashboard

Cases / #56 · 2026-10-17 · Easy

A slow phone on a fast server: CPU throttling over a CDP session

Slow a remote Chromium's main thread 4× and 6× with Emulation.setCPUThrottlingRate, then time a JavaScript workload and a real page load — and see that it's per tab.

Chromium

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

The problem

Fleet browsers run on server CPUs, many times faster than the mid-range phone most of your users carry. A page that feels instant in a test can stall on that phone: long parsing, slow hydration, input that lags. Lighthouse's mobile run throttles the CPU 4× for exactly this reason. Can you do the same on a remote browser, and what does it slow down — and what not?

What we used, and why

WhatWhy
context.newCDPSession(page)A raw CDP channel to the page over the normal Playwright connection (Chromium-based browsers) — no cdp: true needed.
Emulation.setCPUThrottlingRateDevTools' CPU throttling: rate: 4 makes the page's main thread run at a quarter of its speed.
A fixed JavaScript workloadThree million loop steps and a 200,000-item sort, timed with performance.now() inside the page; the same result every time (check), so only the speed changes.
Navigation Timing + Performance.getMetricsResponse received, DOMContentLoaded and load from the page; main-thread busy time (TaskDuration) from Chromium.
A Wikipedia articleA real page with a big DOM (~9,400 nodes) and scripts to parse.

How it works

  1. Launch headless Chromium behind a proxy.
  2. For rates 1×, 4× and 6×: new context, CDP session, set the rate, load the article, read its timings and main-thread time, run the workload.
  3. Finally throttle one tab 6× and run the workload in it and in an unthrottled tab next to it.

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;
const RATES = [1, 4, 6];
const URL = 'https://en.wikipedia.org/wiki/Web_browser';
// A fixed amount of JavaScript work, timed inside the page: what a heavy client-side app does.
const WORK = `(() => {
  const t = performance.now();
  let h = 0;
  for (let i = 0; i < 3e6; i++) h = (h * 31 + i) % 1000003;
  const a = Array.from({ length: 200000 }, (_, i) => (i * 7919) % 100003).sort((x, y) => x - y);
  return { ms: Math.round(performance.now() - t), check: h + a[1000] };
})()`;
const metrics = async (cdp) => Object.fromEntries((await cdp.send('Performance.getMetrics')).metrics.map((m) => [m.name, m.value]));

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

try {
  const rows = [];
  for (const rate of RATES) {
    const context = await browser.newContext();
    const page = await context.newPage();
    // CPU throttling is a DevTools feature: reach it with a CDP session over the Playwright connection.
    const cdp = await context.newCDPSession(page);
    await cdp.send('Performance.enable');
    await cdp.send('Emulation.setCPUThrottlingRate', { rate });
    // A real page load: the network part isn't throttled, the main-thread work is.
    await page.goto(URL, { waitUntil: 'load', timeout: 90000 });
    const nav = await page.evaluate(() => {
      const n = performance.getEntriesByType('navigation')[0];
      return { response_ms: Math.round(n.responseEnd), dcl_ms: Math.round(n.domContentLoadedEventEnd), load_ms: Math.round(n.loadEventEnd) };
    });
    const m = await metrics(cdp);
    const work = await page.evaluate(WORK);
    rows.push({
      rate,
      work_ms: work.ms,
      ...nav,
      parse_ms: nav.dcl_ms - nav.response_ms, // HTML fully received → DOMContentLoaded: parsing and scripts
      main_thread_ms: Math.round(m.TaskDuration * 1000), // busy time while loading, from CDP
      check: work.check,
    });
    await context.close();
  }
  // Is it per page? A throttled tab next to an unthrottled one in the same browser.
  const context = await browser.newContext();
  const slow = await context.newPage();
  const fast = await context.newPage();
  await (await context.newCDPSession(slow)).send('Emulation.setCPUThrottlingRate', { rate: 6 });
  const sideBySide = { throttled_tab_ms: (await slow.evaluate(WORK)).ms, other_tab_ms: (await fast.evaluate(WORK)).ms };
  await context.close();
  console.log(JSON.stringify({ rows, side_by_side: sideBySide }, null, 2));
} finally {
  await browser.close();
}

What we got

CPU slowdownJS workload (ms)HTML → DOMContentLoaded (ms)Main-thread busy (ms)HTML received (ms)Load event (ms)
1×7748642222724615
4×327685144810753230
6×49780521104512398
6×, one tab only494 (other tab: 126)

From the Node.js run on 2026-10-17. 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)
{
  "rows": [
    {
      "rate": 1,
      "work_ms": 77,
      "response_ms": 2272,
      "dcl_ms": 2758,
      "load_ms": 4615,
      "parse_ms": 486,
      "main_thread_ms": 422,
      "check": 488537
    },
    {
      "rate": 4,
      "work_ms": 327,
      "response_ms": 1075,
      "dcl_ms": 1760,
      "load_ms": 3230,
      "parse_ms": 685,
      "main_thread_ms": 1448,
      "check": 488537
    },
    {
      "rate": 6,
      "work_ms": 497,
      "response_ms": 451,
      "dcl_ms": 1256,
      "load_ms": 2398,
      "parse_ms": 805,
      "main_thread_ms": 2110,
      "check": 488537
    }
  ],
  "side_by_side": {
    "throttled_tab_ms": 494,
    "other_tab_ms": 126
  }
}

Takeaways