cdpfleet Docs GitHub Dashboard

Cases / #8 · 2026-09-30 · Medium

A bandwidth diet for residential proxies: what blocking really saves

Count every byte a news homepage pulls through your proxy, then block images, media, fonts and third parties — and see where the money actually goes.

Chromium

Run on production on 2026-09-30: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

Residential proxies bill per gigabyte, so a scraper's cost is mostly bytes. The usual advice is "block images". How much does that save on a real, ad-heavy page — and what else is worth blocking?

What we used, and why

WhatWhy
context.route('**/*') + route.abort()Drops requests before they leave the browser, so they never cost proxy bandwidth.
request.resourceType()Classifies requests: image, media, font, script, xhr, …
A first-party listThe site's own domains and CDNs (bbc.com, bbc.co.uk, bbci.co.uk); everything else is third-party (ads, analytics, trackers).
request.sizes() on requestfinishedExact bytes on the wire per request: request and response headers plus the encoded body.
A fresh context per variantEmpty cache each time, so every variant pays full price.

How it works

  1. Load the page with nothing blocked; count requests and bytes for DOM-ready plus 5 seconds.
  2. Again, blocking images, media and fonts.
  3. Again, also blocking every third-party host.
  4. Convert to proxy cost per 100,000 page loads at $3/GB.

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 PAGE = 'https://www.bbc.com/news';
const FIRST_PARTY = ['bbc.com', 'bbc.co.uk', 'bbci.co.uk']; // the site's own domains and CDNs
const PRICE_PER_GB = 3; // a typical residential proxy price, USD

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: true }),
});
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 } });

const HEAVY = new Set(['image', 'media', 'font']);

// Load the page in a fresh context (empty cache) and count every byte on the wire.
async function measure(label, block) {
  const context = await browser.newContext();
  if (block) {
    await context.route('**/*', (route) => {
      const r = route.request();
      if (HEAVY.has(r.resourceType()) || (block === 'first-party' && !FIRST_PARTY.some((d) => new URL(r.url()).hostname.endsWith(d)))) {
        return route.abort();
      }
      return route.continue();
    });
  }
  const page = await context.newPage();
  let bytes = 0;
  let requests = 0;
  let blocked = 0;
  const count = async (req) => {
    const s = await req.sizes();
    bytes += s.requestHeadersSize + s.requestBodySize + s.responseHeadersSize + s.responseBodySize;
    requests++;
  };
  page.on('requestfinished', count);
  page.on('requestfailed', () => { blocked++; });
  const t = Date.now();
  // DOM ready, then a fixed 5 s for the rest to arrive: the same window for every variant
  // ('load' can wait forever on a blocked video).
  await page.goto(PAGE, { waitUntil: 'domcontentloaded', timeout: 90000 });
  const readyMs = Date.now() - t;
  await page.waitForTimeout(5000);
  const title = await page.title();
  page.off('requestfinished', count); // stop counting before closing
  await context.close();
  return { variant: label, title, requests, blocked, kilobytes: Math.round(bytes / 1024), dom_ready_ms: readyMs };
}

try {
  const rows = [
    await measure('everything', null),
    await measure('no images, media or fonts', 'heavy'),
    await measure('…and first-party only', 'first-party'),
  ];
  const full = rows[0].kilobytes;
  for (const r of rows) {
    r.saved = `${Math.round((1 - r.kilobytes / full) * 100)}%`;
    r.proxy_cost_per_100k_pages = `$${((r.kilobytes * 100000) / 1024 / 1024 * PRICE_PER_GB).toFixed(0)}`;
  }
  console.log(JSON.stringify(rows, null, 2));
} finally {
  await browser.close();
}

What we got

VariantRequestsBlockedKBSavedProxy cost / 100k pagesDOM ready (ms)
everything80016620%$4764314
no images, media or fonts842815338%$4395030
…and first-party only533179152%$2262688

From the Node.js run on 2026-09-30. 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)
[
  {
    "variant": "everything",
    "title": "BBC News - Breaking news, video and the latest top stories from the U.S. and around the world",
    "requests": 80,
    "blocked": 0,
    "kilobytes": 1662,
    "dom_ready_ms": 4314,
    "saved": "0%",
    "proxy_cost_per_100k_pages": "$476"
  },
  {
    "variant": "no images, media or fonts",
    "title": "BBC News - Breaking news, video and the latest top stories from the U.S. and around the world",
    "requests": 84,
    "blocked": 28,
    "kilobytes": 1533,
    "dom_ready_ms": 5030,
    "saved": "8%",
    "proxy_cost_per_100k_pages": "$439"
  },
  {
    "variant": "…and first-party only",
    "title": "BBC News - Breaking news, video and the latest top stories from the U.S. and around the world",
    "requests": 53,
    "blocked": 31,
    "kilobytes": 791,
    "dom_ready_ms": 2688,
    "saved": "52%",
    "proxy_cost_per_100k_pages": "$226"
  }
]

Takeaways