[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #14 · 2026-10-01 · Benchmark · Medium · for [web scraping](https://cdpfleet.com/web-scraping)

# 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](https://cdpfleet.com/docs/browsers/chromium)

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

A benchmark: the results below are what this setup measured on the dates shown, on our own test runs. Sites, networks and browsers change, so rerun the code before you rely on the numbers.

## 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

| What | Why |
| --- | --- |
| Three sessions in parallel | Random residential, residential in Germany, datacenter SOCKS5 — the same page through each. |
| A fresh context per load | Cold 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-paint` | When the main content appeared — closest to "the page is usable". |
| Median of 3 loads | Residential 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](https://github.com/cdpfleet/cdpfleet/tree/main/cases/14-proxy-speed), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)
-   `PROXY_URL` — your proxy, e.g. `http://user:pass@proxy.example.com:8000`
-   `PROXY_URL_DE` — a proxy exit in Germany
-   `SOCKS_PROXIES` — comma-separated `socks5://user:pass@host:port` proxies

 

```node
// npm install playwright@1.60.0
// 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));
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL, PROXY_URL_DE, SOCKS_PROXIES (comma-separated socks5:// URLs)
import json
import os
import statistics
from concurrent.futures import ThreadPoolExecutor

import requests
from playwright.sync_api import sync_playwright

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

# Navigation Timing + Largest Contentful Paint, read in the page after load.
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,
      ttfb: nav.responseStart - nav.requestStart,
      domReady: nav.domContentLoadedEventEnd,
      load: nav.loadEventEnd,
      lcp: lcp.startTime,
    });
  }).observe({ type: 'largest-contentful-paint', buffered: true });
})"""

def measure(item):
    label, proxy = item
    res = requests.post("https://starter.cdpfleet.com/chromium/session", headers={"x-api-key": KEY},
                        json={"proxy": proxy, "headless": True}, timeout=60)
    if not res.ok:
        return {"proxy": label, "error": f"launch {res.status_code}"}
    with sync_playwright() as p:  # the sync API is per thread
        browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
        try:
            runs, failures = [], 0
            while len(runs) < LOADS:
                context = browser.new_context()
                try:
                    page = context.new_page()
                    page.goto(PAGE, wait_until="load", timeout=60000)
                    runs.append(page.evaluate(TIMINGS))
                except Exception:
                    failures += 1
                    if failures > 1:  # one failed load is the proxy's noise; two is a problem
                        raise
                finally:
                    context.close()
            m = lambda k: round(statistics.median(r[k] for r in runs))
            return {"proxy": label, "loads": len(runs), "connect_ms": m("connect"), "ttfb_ms": m("ttfb"),
                    "dom_ready_ms": m("domReady"), "lcp_ms": m("lcp"), "load_ms": m("load")}
        finally:
            browser.close()

with ThreadPoolExecutor(len(PROXIES)) as pool:
    rows = list(pool.map(measure, PROXIES.items()))
print(json.dumps({"page": PAGE, "statistic": f"median of {LOADS} cold loads", "results": rows}, indent=2))
```

```java
// 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, PROXY_URL, PROXY_URL_DE, SOCKS_PROXIES (comma-separated socks5:// URLs)
import com.google.gson.*;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;
import java.util.concurrent.*;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");
  static final String PAGE = "https://en.wikipedia.org/wiki/Web_browser";
  static final int LOADS = 3; // per proxy, each in a fresh context (cold cache, new connections)

  // Navigation Timing + Largest Contentful Paint, read in the page after load.
  static final String 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,
            ttfb: nav.responseStart - nav.requestStart,
            domReady: nav.domContentLoadedEventEnd,
            load: nav.loadEventEnd,
            lcp: lcp.startTime,
          });
        }).observe({ type: 'largest-contentful-paint', buffered: true });
      })""";

  static JsonObject measure(String label, String proxy) throws Exception {
    String body = "{\"proxy\": " + new Gson().toJson(proxy) + ", \"headless\": true}";
    HttpResponse<String> res = HttpClient.newHttpClient().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(body)).build(), HttpResponse.BodyHandlers.ofString());
    JsonObject out = new JsonObject();
    out.addProperty("proxy", label);
    if (res.statusCode() != 200) { out.addProperty("error", "launch " + res.statusCode()); return out; }
    String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
    try (Playwright playwright = Playwright.create()) { // Playwright objects are per thread
      Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
      try {
        List<JsonObject> runs = new ArrayList<>();
        int failures = 0;
        while (runs.size() < LOADS) {
          BrowserContext context = browser.newContext();
          try {
            Page page = context.newPage();
            page.navigate(PAGE, new Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD).setTimeout(60000));
            runs.add(new Gson().toJsonTree(page.evaluate(TIMINGS)).getAsJsonObject());
          } catch (PlaywrightException err) {
            if (++failures > 1) throw err; // one failed load is the proxy's noise; two is a problem
          } finally {
            context.close();
          }
        }
        out.addProperty("loads", runs.size());
        for (String[] k : new String[][] {{"connect", "connect_ms"}, {"ttfb", "ttfb_ms"}, {"domReady", "dom_ready_ms"}, {"lcp", "lcp_ms"}, {"load", "load_ms"}}) {
          double[] v = runs.stream().mapToDouble(r -> r.get(k[0]).getAsDouble()).sorted().toArray();
          out.addProperty(k[1], Math.round(v[v.length / 2])); // median
        }
        return out;
      } finally {
        browser.close();
      }
    }
  }

  public static void main(String[] args) throws Exception {
    Map<String, String> proxies = new LinkedHashMap<>();
    proxies.put("residential (any country)", System.getenv("PROXY_URL"));
    proxies.put("residential (Germany)", System.getenv("PROXY_URL_DE"));
    proxies.put("datacenter SOCKS5", System.getenv("SOCKS_PROXIES").split(",")[0]);
    ExecutorService pool = Executors.newFixedThreadPool(proxies.size());
    List<Future<JsonObject>> jobs = new ArrayList<>();
    for (Map.Entry<String, String> e : proxies.entrySet()) jobs.add(pool.submit(() -> measure(e.getKey(), e.getValue())));
    JsonArray rows = new JsonArray();
    for (Future<JsonObject> f : jobs) rows.add(f.get());
    pool.shutdown();
    JsonObject out = new JsonObject();
    out.addProperty("page", PAGE);
    out.addProperty("statistic", "median of " + LOADS + " cold loads");
    out.add("results", rows);
    System.out.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL, PROXY_URL_DE, SOCKS_PROXIES (comma-separated socks5:// URLs)
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

const string PageUrl = "https://en.wikipedia.org/wiki/Web_browser";
const int Loads = 3; // per proxy, each in a fresh context (cold cache, new connections)
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
var proxies = new (string Label, string Proxy)[]
{
    ("residential (any country)", Environment.GetEnvironmentVariable("PROXY_URL")!),
    ("residential (Germany)", Environment.GetEnvironmentVariable("PROXY_URL_DE")!),
    ("datacenter SOCKS5", Environment.GetEnvironmentVariable("SOCKS_PROXIES")!.Split(',')[0]),
};

// Navigation Timing + Largest Contentful Paint, read in the page after load.
const string 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,
          ttfb: nav.responseStart - nav.requestStart,
          domReady: nav.domContentLoadedEventEnd,
          load: nav.loadEventEnd,
          lcp: lcp.startTime,
        });
      }).observe({ type: 'largest-contentful-paint', buffered: true });
    })
    """;

using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
using var playwright = await Playwright.CreateAsync();

async Task<JsonObject> Measure(string label, string proxy)
{
    var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session", new { proxy, headless = true });
    if (!res.IsSuccessStatusCode) return new JsonObject { ["proxy"] = label, ["error"] = $"launch {(int)res.StatusCode}" };
    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 runs = new List<JsonElement>();
        var failures = 0;
        while (runs.Count < Loads)
        {
            var context = await browser.NewContextAsync();
            try
            {
                var page = await context.NewPageAsync();
                await page.GotoAsync(PageUrl, new() { WaitUntil = WaitUntilState.Load, Timeout = 60000 });
                runs.Add(await page.EvaluateAsync<JsonElement>(Timings));
            }
            catch (Exception err) when ((err is PlaywrightException or TimeoutException) && ++failures <= 1) { } // one failed load is the proxy's noise
            finally { await context.CloseAsync(); }
        }
        long Median(string k) => (long)Math.Round(runs.Select(r => r.GetProperty(k).GetDouble()).OrderBy(v => v).ElementAt(runs.Count / 2));
        return new JsonObject
        {
            ["proxy"] = label, ["loads"] = runs.Count, ["connect_ms"] = Median("connect"), ["ttfb_ms"] = Median("ttfb"),
            ["dom_ready_ms"] = Median("domReady"), ["lcp_ms"] = Median("lcp"), ["load_ms"] = Median("load"),
        };
    }
    finally
    {
        await browser.CloseAsync();
    }
}

var rows = await Task.WhenAll(proxies.Select(p => Measure(p.Label, p.Proxy)));
var result = new JsonObject { ["page"] = PageUrl, ["statistic"] = $"median of {Loads} cold loads", ["results"] = new JsonArray(rows.ToArray<JsonNode?>()) };
Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL, PROXY_URL_DE, SOCKS_PROXIES (comma-separated socks5:// URLs)
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"math"
	"net/http"
	"os"
	"sort"
	"strings"
	"sync"

	"github.com/playwright-community/playwright-go"
)

var key = os.Getenv("CDPFLEET_API_KEY")

const pageURL = "https://en.wikipedia.org/wiki/Web_browser"
const loads = 3 // per proxy, each in a fresh context (cold cache, new connections)

// 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,
      ttfb: nav.responseStart - nav.requestStart,
      domReady: nav.domContentLoadedEventEnd,
      load: nav.loadEventEnd,
      lcp: lcp.startTime,
    });
  }).observe({ type: 'largest-contentful-paint', buffered: true });
})`

func launch(name string, options map[string]any) (map[string]any, error) {
	body, _ := json.Marshal(options)
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/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 nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
	}
	var session map[string]any
	return session, json.NewDecoder(res.Body).Decode(&session)
}

func must[T any](v T, err error) T {
	if err != nil {
		log.Fatal(err)
	}
	return v
}

// Evaluate returns whole numbers as int and others as float64.
func num(v any) float64 {
	switch n := v.(type) {
	case float64:
		return n
	case int:
		return float64(n)
	case int64:
		return float64(n)
	}
	return 0
}

func measure(pw *playwright.Playwright, label, proxy string) map[string]any {
	fail := func(err error) map[string]any {
		return map[string]any{"proxy": label, "error": strings.SplitN(err.Error(), "\n", 2)[0]}
	}
	session, err := launch("chromium", map[string]any{"proxy": proxy, "headless": true})
	if err != nil {
		return fail(err)
	}
	browser, err := pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
	if err != nil {
		return fail(err)
	}
	defer browser.Close()
	runs := []map[string]any{}
	failures := 0
	for len(runs) < loads {
		context, err := browser.NewContext()
		if err != nil {
			return fail(err)
		}
		page, _ := context.NewPage()
		_, err = page.Goto(pageURL, playwright.PageGotoOptions{WaitUntil: playwright.WaitUntilStateLoad, Timeout: playwright.Float(60000)})
		var v any
		if err == nil {
			v, err = page.Evaluate(timings)
		}
		context.Close()
		if err != nil {
			if failures++; failures > 1 { // one failed load is the proxy's noise; two is a problem
				return fail(err)
			}
			continue
		}
		runs = append(runs, v.(map[string]any))
	}
	median := func(k string) int64 {
		xs := []float64{}
		for _, r := range runs {
			xs = append(xs, num(r[k]))
		}
		sort.Float64s(xs)
		return int64(math.Round(xs[len(xs)/2]))
	}
	return map[string]any{"proxy": label, "loads": len(runs), "connect_ms": median("connect"), "ttfb_ms": median("ttfb"),
		"dom_ready_ms": median("domReady"), "lcp_ms": median("lcp"), "load_ms": median("load")}
}

func main() {
	pw := must(playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true}))
	defer pw.Stop()
	proxies := [][2]string{
		{"residential (any country)", os.Getenv("PROXY_URL")},
		{"residential (Germany)", os.Getenv("PROXY_URL_DE")},
		{"datacenter SOCKS5", strings.Split(os.Getenv("SOCKS_PROXIES"), ",")[0]},
	}
	rows := make([]map[string]any, len(proxies))
	var wg sync.WaitGroup
	for i, p := range proxies {
		wg.Add(1)
		go func(i int, label, proxy string) { defer wg.Done(); rows[i] = measure(pw, label, proxy) }(i, p[0], p[1])
	}
	wg.Wait()
	out, _ := json.MarshalIndent(map[string]any{"page": pageURL, "statistic": fmt.Sprintf("median of %d cold loads", loads), "results": rows}, "", "  ")
	fmt.Println(string(out))
}
```

## What we got

| Proxy | Connect (ms) | First byte (ms) | LCP (ms) | DOM ready (ms) | Load (ms) |
| --- | --- | --- | --- | --- | --- |
| residential (any country) | 1614 | 742 | 3384 | 3382 | 7309 |
| residential (Germany) | 340 | 201 | 948 | 970 | 1946 |
| datacenter SOCKS5 | 543 | 119 | 1152 | 1165 | 2441 |

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

-   **Location beats proxy type:** the residential exit in Germany — next to our servers and to the site's European CDN — was the fastest of all, faster than the datacenter proxy.
-   **A random residential exit is the slow, unpredictable one:** connection setup and the full load took several times longer, because traffic crossed continents twice.
-   **Connection setup dominates cold loads:** every new connection is a TCP + TLS handshake through the proxy; reusing sessions and contexts (see the worker-pool case) avoids paying it over and over.
-   Pick proxy countries near your targets, and measure with your real pages — these numbers move with every exit.

---
Source: https://cdpfleet.com/docs/cases/proxy-speed · Markdown: https://cdpfleet.com/docs/cases/proxy-speed.md · All docs for AI tools: https://cdpfleet.com/llms.txt
