Cases / #51 · 2026-10-16 · Medium
Cut residential spend: the page on Tier 1, the images on Tier 4, in one session
The cdpfleet residential proxy has four price tiers, from $5 to $0.50 per GB. Send a gallery page through Tier 1 and its image hosts through Tier 4 with `proxy_rules`, count the bytes of each, and check them against what the balance actually billed per tier.
Run on production on 2026-10-16: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Residential traffic is billed per GB, and on most pages the bulk of the bytes isn't the HTML you came for but images and static files from a CDN. The tier in the cdpfleet-resi token sets the price per GB, so the question is practical: can one session send the page through one tier and the heavy hosts through a cheaper one, how much of a real page is "heavy", and does the bill really follow the split?
What we used, and why
| What | Why |
|---|---|
proxy: "cdpfleet-resi-country-us" | The default route: every host no rule matches. No -tier-N means Tier 1 ($5/GB), US exits. |
proxy_rules → cdpfleet-resi-tier-4-country-us | The two image hosts (thumb.wikimedia.org, upload.wikimedia.org) through Tier 4 ($0.50/GB). A token can be the session's proxy or a single rule's proxy (not part of a list of proxies). |
request.sizes() on requestfinished | Headers and body of every request and response, as the browser saw them, added up per host group. |
GET /v1/me/proxy-balance | used_today lists today's billed bytes per tier. Read before and after, the difference is what this run was billed — metering trails by about a minute, so the program waits for it. |
How it works
- Read today's billed bytes per tier.
- Launch headless Chromium with Tier 1 as the default and a Tier 4 rule for the image hosts.
- Open a Wikimedia Commons category gallery, scroll to the end so the lazy thumbnails load, and add up the bytes of the page host and of the image hosts.
- Close the session, wait for metering, and compare the billed bytes per tier with what the browser counted; work out what 1,000 such loads cost on Tier 1 only and with the split.
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
CDPFLEET_API_KEY— your API key (dashboard → API keys)
// npm install [email protected]
// env: CDPFLEET_API_KEY (no proxy of your own needed: the cdpfleet residential proxy, billed per GB by tier)
import { chromium } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
const PAGE = 'https://commons.wikimedia.org/wiki/Category:Cats';
const PRICE = { 1: 5, 4: 0.5 }; // USD per GB (10^9 bytes)
const CDN = ['thumb.wikimedia.org', 'upload.wikimedia.org']; // the images; HTML, CSS and JS come from commons.wikimedia.org
// One session, two prices: the page through Tier 1, the image hosts through Tier 4.
const LAUNCH = {
headless: 'new',
record: false,
proxy: 'cdpfleet-resi-country-us',
proxy_rules: [{ hosts: CDN, proxy: 'cdpfleet-resi-tier-4-country-us' }],
};
// Today's residential traffic per tier, as billed.
async function usedToday() {
const b = await (await fetch('https://cdpfleet.com/v1/me/proxy-balance', { headers: { 'x-api-key': KEY } })).json();
return Object.fromEntries(b.used_today.map((t) => [t.tier, t.bytes]));
}
// Load the gallery, scroll to the end, and add up every request's bytes by host group.
async function load() {
const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify(LAUNCH),
});
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 page = await browser.newPage();
const done = [];
page.on('requestfinished', (req) => done.push(req));
// Residential peers drop the odd connection: retry the navigation once.
for (let attempt = 1; ; attempt++) {
try { await page.goto(PAGE, { waitUntil: 'load', timeout: 60000 }); break; } catch (err) { if (attempt === 2) throw err; }
}
for (let i = 0; i < 20; i++) { // lazy-loaded thumbnails load as they scroll into view
await page.mouse.wheel(0, 1500);
await page.waitForTimeout(250);
}
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
const bytes = { page: 0, images: 0 };
let images = 0;
for (const req of done) {
const s = await req.sizes().catch(() => null);
if (!s) continue;
const total = s.requestHeadersSize + s.requestBodySize + s.responseHeadersSize + s.responseBodySize;
if (CDN.includes(new URL(req.url()).hostname)) { bytes.images += total; images++; } else bytes.page += total;
}
return { requests: done.length, image_requests: images, page_kb: Math.round(bytes.page / 1000), image_kb: Math.round(bytes.images / 1000) };
} finally {
await browser.close();
}
}
const before = await usedToday();
const run = await load();
// Metering trails the traffic by about a minute: wait until Tier 4 shows the images.
let after = before;
for (let i = 0; i < 18 && ((after[4] || 0) - (before[4] || 0)) / 1000 < run.image_kb * 0.8; i++) {
await new Promise((r) => setTimeout(r, 10000));
after = await usedToday();
}
// What 1,000 such loads cost: everything on Tier 1 versus the split.
const per1000 = (pageTier, imageTier) => Math.round((run.page_kb * PRICE[pageTier] + run.image_kb * PRICE[imageTier]) / 1e6 * 1000 * 100) / 100;
console.log(JSON.stringify({
page: PAGE,
run,
image_share: Math.round((run.image_kb / (run.image_kb + run.page_kb)) * 100) / 100,
cost_per_1000_loads_usd: { all_tier_1: per1000(1, 1), page_tier_1_images_tier_4: per1000(1, 4) },
billed_kb: {
tier_1: Math.round(((after[1] || 0) - (before[1] || 0)) / 1000),
tier_4: Math.round(((after[4] || 0) - (before[4] || 0)) / 1000),
},
}, null, 2));
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY (no proxy of your own needed: the cdpfleet residential proxy, billed per GB by tier)
import json
import math
import os
import time
from urllib.parse import urlparse
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
PAGE = "https://commons.wikimedia.org/wiki/Category:Cats"
PRICE = {1: 5, 4: 0.5} # USD per GB (10^9 bytes)
CDN = ["thumb.wikimedia.org", "upload.wikimedia.org"] # the images; HTML, CSS and JS come from commons.wikimedia.org
# One session, two prices: the page through Tier 1, the image hosts through Tier 4.
LAUNCH = {
"headless": "new",
"record": False,
"proxy": "cdpfleet-resi-country-us",
"proxy_rules": [{"hosts": CDN, "proxy": "cdpfleet-resi-tier-4-country-us"}],
}
def rnd(x):
return math.floor(x + 0.5)
# Today's residential traffic per tier, as billed.
def used_today():
b = requests.get("https://cdpfleet.com/v1/me/proxy-balance", headers={"x-api-key": KEY}, timeout=30).json()
return {t["tier"]: t["bytes"] for t in b["used_today"]}
# Load the gallery, scroll to the end, and add up every request's bytes by host group.
def load(pw):
r = requests.post("https://starter.cdpfleet.com/chromium/session", headers={"x-api-key": KEY}, json=LAUNCH, timeout=120)
if not r.ok:
raise RuntimeError(f"launch {r.status_code} {r.text}")
browser = pw.chromium.connect(r.json()["wsUrl"], headers={"x-api-key": KEY})
try:
page = browser.new_page()
done = []
page.on("requestfinished", lambda req: done.append(req))
# Residential peers drop the odd connection: retry the navigation once.
for attempt in (1, 2):
try:
page.goto(PAGE, wait_until="load", timeout=60000)
break
except Exception:
if attempt == 2:
raise
for _ in range(20): # lazy-loaded thumbnails load as they scroll into view
page.mouse.wheel(0, 1500)
page.wait_for_timeout(250)
try:
page.wait_for_load_state("networkidle", timeout=15000)
except Exception:
pass
page_bytes = image_bytes = images = 0
for req in done:
try:
s = req.sizes()
except Exception:
continue
total = s["requestHeadersSize"] + s["requestBodySize"] + s["responseHeadersSize"] + s["responseBodySize"]
if urlparse(req.url).hostname in CDN:
image_bytes += total
images += 1
else:
page_bytes += total
return {"requests": len(done), "image_requests": images, "page_kb": rnd(page_bytes / 1000), "image_kb": rnd(image_bytes / 1000)}
finally:
browser.close()
before = used_today()
with sync_playwright() as pw:
run = load(pw)
# Metering trails the traffic by about a minute: wait until Tier 4 shows the images.
after = before
for _ in range(18):
if (after.get(4, 0) - before.get(4, 0)) / 1000 >= run["image_kb"] * 0.8:
break
time.sleep(10)
after = used_today()
# What 1,000 such loads cost: everything on Tier 1 versus the split.
def per1000(page_tier, image_tier):
return rnd((run["page_kb"] * PRICE[page_tier] + run["image_kb"] * PRICE[image_tier]) / 1e6 * 1000 * 100) / 100
print(json.dumps({
"page": PAGE,
"run": run,
"image_share": rnd(run["image_kb"] / (run["image_kb"] + run["page_kb"]) * 100) / 100,
"cost_per_1000_loads_usd": {"all_tier_1": per1000(1, 1), "page_tier_1_images_tier_4": per1000(1, 4)},
"billed_kb": {
"tier_1": rnd((after.get(1, 0) - before.get(1, 0)) / 1000),
"tier_4": rnd((after.get(4, 0) - before.get(4, 0)) / 1000),
},
}, indent=2))
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1.
// env: CDPFLEET_API_KEY (no proxy of your own needed: the cdpfleet residential proxy, billed per GB by tier)
import com.google.gson.*;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
static final String PAGE = "https://commons.wikimedia.org/wiki/Category:Cats";
static final Map<Integer, Double> PRICE = Map.of(1, 5.0, 4, 0.5); // USD per GB (10^9 bytes)
static final List<String> CDN = List.of("thumb.wikimedia.org", "upload.wikimedia.org"); // the images; HTML, CSS and JS come from commons.wikimedia.org
static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
static final HttpClient HTTP = HttpClient.newHttpClient();
// One session, two prices: the page through Tier 1, the image hosts through Tier 4.
static JsonObject launchBody() {
JsonObject rule = new JsonObject();
rule.add("hosts", GSON.toJsonTree(CDN));
rule.addProperty("proxy", "cdpfleet-resi-tier-4-country-us");
JsonArray rules = new JsonArray();
rules.add(rule);
JsonObject body = new JsonObject();
body.addProperty("headless", "new");
body.addProperty("record", false);
body.addProperty("proxy", "cdpfleet-resi-country-us");
body.add("proxy_rules", rules);
return body;
}
// Today's residential traffic per tier, as billed.
static Map<Integer, Long> usedToday() throws Exception {
HttpResponse<String> res = HTTP.send(HttpRequest.newBuilder(URI.create("https://cdpfleet.com/v1/me/proxy-balance"))
.header("x-api-key", KEY).GET().build(), HttpResponse.BodyHandlers.ofString());
Map<Integer, Long> used = new HashMap<>();
for (JsonElement t : JsonParser.parseString(res.body()).getAsJsonObject().getAsJsonArray("used_today")) {
used.put(t.getAsJsonObject().get("tier").getAsInt(), t.getAsJsonObject().get("bytes").getAsLong());
}
return used;
}
// Load the gallery, scroll to the end, and add up every request's bytes by host group.
static JsonObject load(Playwright playwright) throws Exception {
HttpResponse<String> res = HTTP.send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(launchBody().toString())).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) throw new RuntimeException("launch " + res.statusCode() + " " + res.body());
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
Page page = browser.newPage();
List<Request> done = new ArrayList<>();
page.onRequestFinished(done::add);
// Residential peers drop the odd connection: retry the navigation once.
for (int attempt = 1; ; attempt++) {
try {
page.navigate(PAGE, new Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD).setTimeout(60000));
break;
} catch (PlaywrightException e) {
if (attempt == 2) throw e;
}
}
for (int i = 0; i < 20; i++) { // lazy-loaded thumbnails load as they scroll into view
page.mouse().wheel(0, 1500);
page.waitForTimeout(250);
}
try {
page.waitForLoadState(LoadState.NETWORKIDLE, new Page.WaitForLoadStateOptions().setTimeout(15000));
} catch (PlaywrightException ignored) {
}
long pageBytes = 0, imageBytes = 0;
int images = 0;
for (Request req : new ArrayList<>(done)) {
Sizes s;
try {
s = req.sizes();
} catch (PlaywrightException e) {
continue;
}
long total = s.requestHeadersSize + s.requestBodySize + s.responseHeadersSize + s.responseBodySize;
if (CDN.contains(URI.create(req.url()).getHost())) {
imageBytes += total;
images++;
} else {
pageBytes += total;
}
}
JsonObject run = new JsonObject();
run.addProperty("requests", done.size());
run.addProperty("image_requests", images);
run.addProperty("page_kb", Math.round(pageBytes / 1000.0));
run.addProperty("image_kb", Math.round(imageBytes / 1000.0));
return run;
} finally {
browser.close();
}
}
public static void main(String[] args) throws Exception {
Map<Integer, Long> before = usedToday();
JsonObject run;
try (Playwright playwright = Playwright.create()) {
run = load(playwright);
}
long pageKb = run.get("page_kb").getAsLong();
long imageKb = run.get("image_kb").getAsLong();
// Metering trails the traffic by about a minute: wait until Tier 4 shows the images.
Map<Integer, Long> after = before;
for (int i = 0; i < 18 && (after.getOrDefault(4, 0L) - before.getOrDefault(4, 0L)) / 1000.0 < imageKb * 0.8; i++) {
Thread.sleep(10000);
after = usedToday();
}
// What 1,000 such loads cost: everything on Tier 1 versus the split.
JsonObject cost = new JsonObject();
cost.addProperty("all_tier_1", Math.round((pageKb * PRICE.get(1) + imageKb * PRICE.get(1)) / 1e6 * 1000 * 100) / 100.0);
cost.addProperty("page_tier_1_images_tier_4", Math.round((pageKb * PRICE.get(1) + imageKb * PRICE.get(4)) / 1e6 * 1000 * 100) / 100.0);
JsonObject billed = new JsonObject();
billed.addProperty("tier_1", Math.round((after.getOrDefault(1, 0L) - before.getOrDefault(1, 0L)) / 1000.0));
billed.addProperty("tier_4", Math.round((after.getOrDefault(4, 0L) - before.getOrDefault(4, 0L)) / 1000.0));
JsonObject doc = new JsonObject();
doc.addProperty("page", PAGE);
doc.add("run", run);
doc.addProperty("image_share", Math.round((double) imageKb / (imageKb + pageKb) * 100) / 100.0);
doc.add("cost_per_1000_loads_usd", cost);
doc.add("billed_kb", billed);
System.out.println(GSON.toJson(doc));
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY (no proxy of your own needed: the cdpfleet residential proxy, billed per GB by tier)
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
const string PageUrl = "https://commons.wikimedia.org/wiki/Category:Cats";
var price = new Dictionary<int, double> { [1] = 5, [4] = 0.5 }; // USD per GB (10^9 bytes)
string[] cdn = ["thumb.wikimedia.org", "upload.wikimedia.org"]; // the images; HTML, CSS and JS come from commons.wikimedia.org
// One session, two prices: the page through Tier 1, the image hosts through Tier 4.
var launch = new
{
headless = "new",
record = false,
proxy = "cdpfleet-resi-country-us",
proxy_rules = new[] { new { hosts = cdn, proxy = "cdpfleet-resi-tier-4-country-us" } },
};
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
static long Round(double x) => (long)Math.Floor(x + 0.5);
// Today's residential traffic per tier, as billed.
async Task<Dictionary<int, long>> UsedToday()
{
var b = await http.GetFromJsonAsync<JsonElement>("https://cdpfleet.com/v1/me/proxy-balance");
return b.GetProperty("used_today").EnumerateArray().ToDictionary(t => t.GetProperty("tier").GetInt32(), t => t.GetProperty("bytes").GetInt64());
}
// Load the gallery, scroll to the end, and add up every request's bytes by host group.
async Task<JsonObject> Load(IPlaywright playwright)
{
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session", launch);
if (!res.IsSuccessStatusCode) throw new Exception($"launch {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
var page = await browser.NewPageAsync();
var done = new List<IRequest>();
page.RequestFinished += (_, req) => { lock (done) done.Add(req); };
// Residential peers drop the odd connection: retry the navigation once.
for (var attempt = 1; ; attempt++)
{
try { await page.GotoAsync(PageUrl, new() { WaitUntil = WaitUntilState.Load, Timeout = 60000 }); break; }
catch (Exception e) when (e is PlaywrightException or TimeoutException) { if (attempt == 2) throw; }
}
for (var i = 0; i < 20; i++) // lazy-loaded thumbnails load as they scroll into view
{
await page.Mouse.WheelAsync(0, 1500);
await page.WaitForTimeoutAsync(250);
}
try { await page.WaitForLoadStateAsync(LoadState.NetworkIdle, new() { Timeout = 15000 }); }
catch (TimeoutException) { }
long pageBytes = 0, imageBytes = 0;
var images = 0;
IRequest[] finished;
lock (done) finished = done.ToArray();
foreach (var req in finished)
{
RequestSizesResult s;
try { s = await req.SizesAsync(); }
catch (PlaywrightException) { continue; }
long total = s.RequestHeadersSize + s.RequestBodySize + s.ResponseHeadersSize + s.ResponseBodySize;
if (cdn.Contains(new Uri(req.Url).Host)) { imageBytes += total; images++; } else pageBytes += total;
}
return new JsonObject
{
["requests"] = finished.Length,
["image_requests"] = images,
["page_kb"] = Round(pageBytes / 1000.0),
["image_kb"] = Round(imageBytes / 1000.0),
};
}
finally
{
await browser.CloseAsync();
}
}
var before = await UsedToday();
JsonObject run;
using (var playwright = await Playwright.CreateAsync()) run = await Load(playwright);
var pageKb = (long)run["page_kb"]!;
var imageKb = (long)run["image_kb"]!;
// Metering trails the traffic by about a minute: wait until Tier 4 shows the images.
var after = before;
for (var i = 0; i < 18 && (after.GetValueOrDefault(4) - before.GetValueOrDefault(4)) / 1000.0 < imageKb * 0.8; i++)
{
await Task.Delay(10000);
after = await UsedToday();
}
// What 1,000 such loads cost: everything on Tier 1 versus the split.
double Per1000(int pageTier, int imageTier) => Round((pageKb * price[pageTier] + imageKb * price[imageTier]) / 1e6 * 1000 * 100) / 100.0;
var doc = new JsonObject
{
["page"] = PageUrl,
["run"] = run,
["image_share"] = Round((double)imageKb / (imageKb + pageKb) * 100) / 100.0,
["cost_per_1000_loads_usd"] = new JsonObject { ["all_tier_1"] = Per1000(1, 1), ["page_tier_1_images_tier_4"] = Per1000(1, 4) },
["billed_kb"] = new JsonObject
{
["tier_1"] = Round((after.GetValueOrDefault(1) - before.GetValueOrDefault(1)) / 1000.0),
["tier_4"] = Round((after.GetValueOrDefault(4) - before.GetValueOrDefault(4)) / 1000.0),
},
};
Console.WriteLine(doc.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY (no proxy of your own needed: the cdpfleet residential proxy, billed per GB by tier)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"net/url"
"os"
"slices"
"sync"
"time"
"github.com/playwright-community/playwright-go"
)
const pageURL = "https://commons.wikimedia.org/wiki/Category:Cats"
var (
key = os.Getenv("CDPFLEET_API_KEY")
price = map[int]float64{1: 5, 4: 0.5} // USD per GB (10^9 bytes)
cdn = []string{"thumb.wikimedia.org", "upload.wikimedia.org"} // the images; HTML, CSS and JS come from commons.wikimedia.org
// One session, two prices: the page through Tier 1, the image hosts through Tier 4.
launch = map[string]any{
"headless": "new",
"record": false,
"proxy": "cdpfleet-resi-country-us",
"proxy_rules": []map[string]any{{"hosts": cdn, "proxy": "cdpfleet-resi-tier-4-country-us"}},
}
)
func round(x float64) int64 { return int64(math.Floor(x + 0.5)) }
// Today's residential traffic per tier, as billed.
func usedToday() (map[int]int64, error) {
req, _ := http.NewRequest("GET", "https://cdpfleet.com/v1/me/proxy-balance", nil)
req.Header.Set("x-api-key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var b struct {
UsedToday []struct {
Tier int `json:"tier"`
Bytes int64 `json:"bytes"`
} `json:"used_today"`
}
if err := json.NewDecoder(res.Body).Decode(&b); err != nil {
return nil, err
}
used := map[int]int64{}
for _, t := range b.UsedToday {
used[t.Tier] = t.Bytes
}
return used, nil
}
type runOut struct {
Requests int `json:"requests"`
ImageRequests int `json:"image_requests"`
PageKB int64 `json:"page_kb"`
ImageKB int64 `json:"image_kb"`
}
// Load the gallery, scroll to the end, and add up every request's bytes by host group.
func load(pw *playwright.Playwright) (runOut, error) {
body, _ := json.Marshal(launch)
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/chromium/session", bytes.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return runOut{}, err
}
b, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode != 200 {
return runOut{}, fmt.Errorf("launch %d %s", res.StatusCode, b)
}
var session struct {
WsURL string `json:"wsUrl"`
}
if err := json.Unmarshal(b, &session); err != nil {
return runOut{}, err
}
browser, err := pw.Chromium.Connect(session.WsURL, playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
return runOut{}, err
}
defer browser.Close()
page, err := browser.NewPage()
if err != nil {
return runOut{}, err
}
var mu sync.Mutex
var done []playwright.Request
page.OnRequestFinished(func(r playwright.Request) { mu.Lock(); done = append(done, r); mu.Unlock() })
// Residential peers drop the odd connection: retry the navigation once.
for attempt := 1; ; attempt++ {
_, err = page.Goto(pageURL, playwright.PageGotoOptions{WaitUntil: playwright.WaitUntilStateLoad, Timeout: playwright.Float(60000)})
if err == nil {
break
}
if attempt == 2 {
return runOut{}, err
}
}
for i := 0; i < 20; i++ { // lazy-loaded thumbnails load as they scroll into view
page.Mouse().Wheel(0, 1500)
page.WaitForTimeout(250)
}
page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{State: playwright.LoadStateNetworkidle, Timeout: playwright.Float(15000)})
mu.Lock()
finished := slices.Clone(done)
mu.Unlock()
var pageBytes, imageBytes int64
images := 0
for _, r := range finished {
s, err := r.Sizes()
if err != nil {
continue
}
total := int64(s.RequestHeadersSize + s.RequestBodySize + s.ResponseHeadersSize + s.ResponseBodySize)
u, _ := url.Parse(r.URL())
if slices.Contains(cdn, u.Hostname()) {
imageBytes += total
images++
} else {
pageBytes += total
}
}
return runOut{len(finished), images, round(float64(pageBytes) / 1000), round(float64(imageBytes) / 1000)}, nil
}
func run() error {
before, err := usedToday()
if err != nil {
return err
}
pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
if err != nil {
return err
}
r, err := load(pw)
pw.Stop()
if err != nil {
return err
}
// Metering trails the traffic by about a minute: wait until Tier 4 shows the images.
after := before
for i := 0; i < 18 && float64(after[4]-before[4])/1000 < float64(r.ImageKB)*0.8; i++ {
time.Sleep(10 * time.Second)
if after, err = usedToday(); err != nil {
return err
}
}
// What 1,000 such loads cost: everything on Tier 1 versus the split.
per1000 := func(pageTier, imageTier int) float64 {
return float64(round((float64(r.PageKB)*price[pageTier]+float64(r.ImageKB)*price[imageTier])/1e6*1000*100)) / 100
}
out := struct {
Page string `json:"page"`
Run runOut `json:"run"`
ImageShare float64 `json:"image_share"`
Cost struct {
AllTier1 float64 `json:"all_tier_1"`
Split float64 `json:"page_tier_1_images_tier_4"`
} `json:"cost_per_1000_loads_usd"`
Billed struct {
Tier1 int64 `json:"tier_1"`
Tier4 int64 `json:"tier_4"`
} `json:"billed_kb"`
}{Page: pageURL, Run: r, ImageShare: float64(round(float64(r.ImageKB)/float64(r.ImageKB+r.PageKB)*100)) / 100}
out.Cost.AllTier1, out.Cost.Split = per1000(1, 1), per1000(1, 4)
out.Billed.Tier1, out.Billed.Tier4 = round(float64(after[1]-before[1])/1000), round(float64(after[4]-before[4])/1000)
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(out); err != nil {
return err
}
fmt.Print(buf.String())
return nil
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
What we got
| Measured | Result |
|---|---|
| Requests | 223, of which 172 to the image hosts |
| Bytes seen by the browser | page host 849 KB (Tier 1), image hosts 1856 KB (Tier 4) — images 69 % |
| Bytes billed for the run | Tier 1 838 KB, Tier 4 1747 KB |
| 1,000 loads, all on Tier 1 | $13.53 |
| 1,000 loads, images on Tier 4 | $5.17 (−62 %) |
From the Node.js run on 2026-10-16. 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://commons.wikimedia.org/wiki/Category:Cats",
"run": {
"requests": 223,
"image_requests": 172,
"page_kb": 849,
"image_kb": 1856
},
"image_share": 0.69,
"cost_per_1000_loads_usd": {
"all_tier_1": 13.53,
"page_tier_1_images_tier_4": 5.17
},
"billed_kb": {
"tier_1": 838,
"tier_4": 1747
}
}Takeaways
- Most of the page was images: about two thirds of the bytes came from the two image hosts — the HTML, CSS and JavaScript from the page's own host were the smaller part.
- The bill followed the rule: the Tier 4 bytes billed for the run matched the image bytes the browser counted, and the page's bytes landed on Tier 1. Each GB is charged at the tier of the token that carried it, so one session can mix prices.
- The arithmetic: with the images on Tier 4 a thousand loads of this page cost well under half of what they cost on Tier 1 alone (the table has the run's numbers). The saving is the image share times 90 % — the bigger the share of CDN bytes, the more a split pays.
- Choose by what matters, not only by price: keep the pages that decide whether a scrape works (logins, protected pages) on the tier you trust for them, and move bulk traffic — images, fonts, static files — to a cheaper one. The pools behind each tier can differ and may change; country and sticky sessions work on every tier, device OS on Tier 1 and 2.
- Cheaper still: bytes you never load cost nothing — block what you don't need (case 08). Tiers, balance and token syntax: Residential proxy.