cdpfleet Docs Sign up free

Examples & benchmarks / #7 · 2026-09-30 · Example · Medium · for web scraping

Different sites, different proxies, one browser: proxy_rules

Route one host through a SOCKS5 proxy, another through a round-robin pool and everything else through residential — and the IP-lookup hosts only Camoufox keeps on the default proxy.

Chromium

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

The problem

Residential bandwidth is expensive; datacenter proxies are cheap but get blocked on the sites that matter. A common pattern: send the protected site through residential, static assets or APIs through cheap proxies. proxy_rules does this inside one browser — let's check each rule really exits where it should.

What we used, and why

WhatWhy
proxyThe default route: every host no rule matches (residential here).
proxy_rules rule 1*.ident.me and ident.me → one SOCKS5 proxy. Host patterns take * wildcards.
proxy_rules rule 2httpbin.org → a list of two proxies, used round-robin per connection.
proxy_rules rule 3api.ipify.org → the same SOCKS5 proxy as rule 1. An IP-lookup host: rules for it apply on every browser but Camoufox — see below.
A new context per readingEach context has its own connection pool, so every reading is a new connection and the round-robin shows.

How it works

  1. Launch with a default proxy and three rules.
  2. Read the exit IP from each echo service, each time in a fresh context (one retry if a proxy drops the connection).
  3. Compare each exit with the proxy its rule names.

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

const KEY = process.env.CDPFLEET_API_KEY;
const [socksA, socksB, socksC] = process.env.SOCKS_PROXIES.split(',');

const options = {
  proxy: process.env.PROXY_URL, // everything not matched below
  proxy_rules: [
    { hosts: ['*.ident.me', 'ident.me'], proxy: socksA },
    { hosts: ['httpbin.org', '*.httpbin.org'], proxy: [socksB, socksC] }, // round-robin
    // An IP-lookup host: rules for it apply on every browser but Camoufox.
    { hosts: ['api.ipify.org'], proxy: socksA },
  ],
  headless: true,
};

const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify(options),
});
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 } });

// Each context has its own connection pool, so each reading is a fresh connection.
// Proxies drop a connection now and then: one retry.
async function exitIpVia(url) {
  for (let attempt = 1; ; attempt++) {
    const context = await browser.newContext();
    try {
      const page = await context.newPage();
      const text = await (await page.goto(url, { timeout: 30000 })).text();
      return text.match(/\d{1,3}(\.\d{1,3}){3}/)?.[0] ?? `(no IP in ${url})`;
    } catch (err) {
      if (attempt === 2) return `(failed: ${err.message.split('\n')[0]})`;
    } finally {
      await context.close();
    }
  }
}

try {
  const readings = [];
  for (const url of ['https://v4.ident.me/', 'https://httpbin.org/ip', 'https://httpbin.org/ip', 'https://httpbin.org/ip', 'https://api.ipify.org/', 'https://www.cloudflare.com/cdn-cgi/trace']) {
    readings.push({ url, exit_ip: await exitIpVia(url) });
  }
  console.log(JSON.stringify({
    rules: {
      '*.ident.me': 'SOCKS proxy A',
      'httpbin.org': ['SOCKS proxy B', 'SOCKS proxy C'],
      'api.ipify.org': 'SOCKS proxy A',
      '(everything else)': 'residential PROXY_URL',
    },
    readings,
  }, null, 2));
} finally {
  await browser.close();
}

What we got

URLExit IPRule
https://v4.ident.me/203.0.113.1SOCKS proxy A
https://httpbin.org/ip203.0.113.2SOCKS proxy B or SOCKS proxy C
https://httpbin.org/ip203.0.113.3SOCKS proxy B or SOCKS proxy C
https://httpbin.org/ip203.0.113.4SOCKS proxy B or SOCKS proxy C
https://api.ipify.org/203.0.113.1SOCKS proxy A
https://www.cloudflare.com/cdn-cgi/trace203.0.113.5default proxy

From the Node.js run on 2026-10-17. 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)
{
  "rules": {
    "*.ident.me": "SOCKS proxy A",
    "httpbin.org": [
      "SOCKS proxy B",
      "SOCKS proxy C"
    ],
    "api.ipify.org": "SOCKS proxy A",
    "(everything else)": "residential PROXY_URL"
  },
  "readings": [
    {
      "url": "https://v4.ident.me/",
      "exit_ip": "203.0.113.1"
    },
    {
      "url": "https://httpbin.org/ip",
      "exit_ip": "203.0.113.2"
    },
    {
      "url": "https://httpbin.org/ip",
      "exit_ip": "203.0.113.3"
    },
    {
      "url": "https://httpbin.org/ip",
      "exit_ip": "203.0.113.4"
    },
    {
      "url": "https://api.ipify.org/",
      "exit_ip": "203.0.113.1"
    },
    {
      "url": "https://www.cloudflare.com/cdn-cgi/trace",
      "exit_ip": "203.0.113.5"
    }
  ]
}

Takeaways