cdpfleet Docs GitHub Dashboard

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

Real-time sites through a proxy: WebSocket latency by proxy

Chats, live prices and dashboards run on WebSockets. They work through every proxy type — but each message pays the full proxy path, and location decides how much.

Chromium

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

The problem

Live sites push data over WebSockets, and your browser's WebSocket goes through your proxy like everything else. Does it work through residential and SOCKS proxies, and how much latency does each add to every message?

What we used, and why

WhatWhy
Three sessions, three proxiesA random residential exit, a residential exit in Germany, and a datacenter SOCKS5 proxy.
wss://ws.postman-echo.com/rawA public echo server: every message comes straight back.
page.evaluate with WebSocketThe browser opens the socket itself (through the proxy), times the handshake and 20 sequential round trips.
ip-api.comWhere each proxy exits, to explain the numbers.

How it works

  1. For each proxy, launch a session and look up its exit.
  2. From a real https page, open the WebSocket and time the connection.
  3. Send 20 messages one at a time and record each round trip; report the median and 95th percentile.

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 ECHO = 'wss://ws.postman-echo.com/raw'; // a public WebSocket echo server
const PROXIES = {
  'residential (any country)': process.env.PROXY_URL,
  'residential (Germany)': process.env.PROXY_URL_DE,
  'datacenter SOCKS5': process.env.SOCKS_PROXIES.split(',')[0],
};

// Runs in the page: connect, then 20 echo round trips, one at a time.
const PROBE = async (url) => {
  const t0 = performance.now();
  const ws = new WebSocket(url);
  await new Promise((ok, fail) => { ws.onopen = ok; ws.onerror = () => fail(new Error('WebSocket failed')); });
  const connectMs = performance.now() - t0;
  const rtts = [];
  for (let i = 0; i < 20; i++) {
    const sent = performance.now();
    const echoed = new Promise((ok) => { ws.onmessage = (m) => ok(m.data); });
    ws.send(`ping ${i}`);
    if ((await echoed) !== `ping ${i}`) throw new Error('wrong echo');
    rtts.push(performance.now() - sent);
  }
  ws.close();
  rtts.sort((a, b) => a - b);
  return { connectMs, p50: rtts[10], p95: rtts[18], min: rtts[0] };
};

// One session per proxy. Residential exits drop a connection now and then: one retry
// in a new session, then report the error instead of failing everything.
async function measure(label, proxy) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await measureOnce(label, proxy);
    } catch (err) {
      if (attempt === 2) return { proxy: label, error: err.message.split('\n')[0] };
    }
  }
}

async function measureOnce(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) throw new Error(`launch ${res.status}`);
  const { wsUrl } = await res.json();
  const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
  try {
    const page = await browser.newPage();
    // Where this proxy exits (also gives the page an https origin to open the socket from).
    const geo = await (await page.goto('http://ip-api.com/json/?fields=country,city', { timeout: 30000 })).json();
    await page.goto('https://httpbin.org/html', { timeout: 30000 });
    const r = await page.evaluate(PROBE, ECHO);
    return {
      proxy: label, exit: `${geo.city}, ${geo.country}`,
      connect_ms: Math.round(r.connectMs), rtt_p50_ms: Math.round(r.p50), rtt_p95_ms: Math.round(r.p95), rtt_min_ms: Math.round(r.min),
    };
  } finally {
    await browser.close();
  }
}

const rows = [];
for (const [label, proxy] of Object.entries(PROXIES)) rows.push(await measure(label, proxy));
console.log(JSON.stringify({ echo_server: ECHO, round_trips: 20, results: rows }, null, 2));

What we got

ProxyExitConnect (ms)Round trip p50 (ms)p95 (ms)
residential (any country)Ho Chi Minh City, Vietnam676185196
residential (Germany)Hagen, Germany575142147
datacenter SOCKS5Los Angeles, United States662110120

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)
{
  "echo_server": "wss://ws.postman-echo.com/raw",
  "round_trips": 20,
  "results": [
    {
      "proxy": "residential (any country)",
      "exit": "Ho Chi Minh City, Vietnam",
      "connect_ms": 676,
      "rtt_p50_ms": 185,
      "rtt_p95_ms": 196,
      "rtt_min_ms": 182
    },
    {
      "proxy": "residential (Germany)",
      "exit": "Hagen, Germany",
      "connect_ms": 575,
      "rtt_p50_ms": 142,
      "rtt_p95_ms": 147,
      "rtt_min_ms": 139
    },
    {
      "proxy": "datacenter SOCKS5",
      "exit": "Los Angeles, United States",
      "connect_ms": 662,
      "rtt_p50_ms": 110,
      "rtt_p95_ms": 120,
      "rtt_min_ms": 109
    }
  ]
}

Takeaways