cdpfleet Docs GitHub Dashboard

Cases / #14 · 2026-10-01 · Medium

What your proxy costs you in page speed

The same page, cold, through three proxies: connection setup, time to first byte, largest paint and full load — measured by the browser itself.

Chromium

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

The problem

Slow scrapes are usually blamed on the browser, but most of the time goes into the network path through the proxy. How much does the proxy choice change the numbers that matter — and is "residential" always the slow one?

What we used, and why

WhatWhy
Three sessions in parallelRandom residential, residential in Germany, datacenter SOCKS5 — the same page through each.
A fresh context per loadCold cache and new connections every time, so every load pays the full setup.
Navigation Timing (performance.getEntriesByType('navigation'))Connection setup through the proxy tunnel, time to first byte, DOM ready and load, as the browser recorded them.
PerformanceObserver for largest-contentful-paintWhen the main content appeared — closest to "the page is usable".
Median of 3 loadsResidential exits vary; the median smooths one-off spikes.

How it works

  1. Launch one session per proxy, all at once.
  2. Load the page three times per proxy, each in a new context, and read the timings in the page.
  3. Report the median per proxy.

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, PROXY_URL_DE, SOCKS_PROXIES (comma-separated socks5:// URLs)
import { chromium } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const PAGE = 'https://en.wikipedia.org/wiki/Web_browser';
const LOADS = 3; // per proxy, each in a fresh context (cold cache, new connections)
const PROXIES = {
  'residential (any country)': process.env.PROXY_URL,
  'residential (Germany)': process.env.PROXY_URL_DE,
  'datacenter SOCKS5': process.env.SOCKS_PROXIES.split(',')[0],
};

// Navigation Timing + Largest Contentful Paint, read in the page after load.
const TIMINGS = () => new Promise((done) => {
  const nav = performance.getEntriesByType('navigation')[0];
  new PerformanceObserver((list) => {
    const lcp = list.getEntries().at(-1);
    done({
      connect: nav.connectEnd - nav.connectStart, // TCP + TLS through the proxy tunnel
      ttfb: nav.responseStart - nav.requestStart,  // first byte after the request was sent
      domReady: nav.domContentLoadedEventEnd,
      load: nav.loadEventEnd,
      lcp: lcp.startTime,
    });
  }).observe({ type: 'largest-contentful-paint', buffered: true });
});

const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];

async function measure(label, proxy) {
  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, headless: true }),
  });
  if (!res.ok) return { proxy: label, error: `launch ${res.status}` };
  const { wsUrl } = await res.json();
  const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
  try {
    const runs = [];
    for (let i = 0, failures = 0; i < LOADS; i++) {
      const context = await browser.newContext();
      try {
        const page = await context.newPage();
        await page.goto(PAGE, { waitUntil: 'load', timeout: 60000 });
        runs.push(await page.evaluate(TIMINGS));
      } catch (err) {
        if (++failures > 1) throw err; // one failed load is the proxy's noise; two is a problem
        i--;
      } finally {
        await context.close();
      }
    }
    const m = (k) => Math.round(median(runs.map((r) => r[k])));
    return { proxy: label, loads: runs.length, connect_ms: m('connect'), ttfb_ms: m('ttfb'), dom_ready_ms: m('domReady'), lcp_ms: m('lcp'), load_ms: m('load') };
  } finally {
    await browser.close();
  }
}

const rows = await Promise.all(Object.entries(PROXIES).map(([label, proxy]) => measure(label, proxy)));
console.log(JSON.stringify({ page: PAGE, statistic: `median of ${LOADS} cold loads`, results: rows }, null, 2));

What we got

ProxyConnect (ms)First byte (ms)LCP (ms)DOM ready (ms)Load (ms)
residential (any country)1614742338433827309
residential (Germany)3402019489701946
datacenter SOCKS5543119115211652441

From the Node.js run on 2026-10-01. 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)
{
  "page": "https://en.wikipedia.org/wiki/Web_browser",
  "statistic": "median of 3 cold loads",
  "results": [
    {
      "proxy": "residential (any country)",
      "loads": 3,
      "connect_ms": 1614,
      "ttfb_ms": 742,
      "dom_ready_ms": 3382,
      "lcp_ms": 3384,
      "load_ms": 7309
    },
    {
      "proxy": "residential (Germany)",
      "loads": 3,
      "connect_ms": 340,
      "ttfb_ms": 201,
      "dom_ready_ms": 970,
      "lcp_ms": 948,
      "load_ms": 1946
    },
    {
      "proxy": "datacenter SOCKS5",
      "loads": 3,
      "connect_ms": 543,
      "ttfb_ms": 119,
      "dom_ready_ms": 1165,
      "lcp_ms": 1152,
      "load_ms": 2441
    }
  ]
}

Takeaways