cdpfleet Docs Sign up free

Examples & benchmarks / #68 · 2026-10-24 · Benchmark · Easy · for web scraping

CAPTCHA demo pages: what reCAPTCHA v3 scores and hCaptcha show seven browsers, and why a demo can't rank them

Google's public reCAPTCHA v3 score demo and hCaptcha's demo widget, loaded once per cell by seven browser setups through your own proxy and a sticky cdpfleet-resi exit. The score each setup got, whether the hCaptcha widget rendered, and why vendors' demo pages tell you little about how a real site will treat your browser.

Chromium Google Chrome Microsoft Edge Brave Camoufox Firefox WebKit

Run on production on 2026-10-24: ✓ 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

reCAPTCHA v3 gives every visitor a score from 0.0 (likely a bot) to 1.0 (likely a person) without showing anything, and the site decides what to do with it. People often open Google's public v3 demo to "check their score", and hCaptcha's demo to see whether a challenge appears. This case loads both demos once per browser setup and network and records what they report. Nothing is clicked, solved or retried; it is a dated measurement on the vendors' own demo pages, not an evasion guide. For the signals a page can read, see What a page can read about your browser.

What we used, and why

WhatWhy
Seven setupsHeadless Chromium; headful Google Chrome, Microsoft Edge and Brave; Camoufox and Firefox (both based on Firefox source; not affiliated with Mozilla); headful WebKit. Every launch uses record: false.
proxy: your own vs cdpfleet-resi-country-us-session-<id>Your own rotating proxy against one sticky residential exit shared by every setup, so a difference between rows on that network is the browser, not the IP.
proxy_updatable on WebKitWebKit takes an authenticated HTTP proxy through the updatable route (see WebKit).
A fresh context per pageEvery load is a first visit, with no Google or hCaptcha cookies from an earlier page.
page.evaluate() once a secondGoogle's demo prints its server's verify response, including "score", on the page; the hCaptcha demo is checked for its widget frame. Nothing on either page is touched.

How it works

  1. For each network (your own proxy, then the sticky Tier 1 exit) and each of the seven setups, launch one session.
  2. Load Google's reCAPTCHA v3 demo in a fresh context and wait up to 20 s for the score it prints.
  3. Load hCaptcha's demo in another fresh context and wait up to 12 s for its widget to render. Do not click it.

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 (one page load per page per setup and network; nothing is clicked or solved)
import { randomBytes } from 'node:crypto';
import { chromium, firefox, webkit } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const RUN = randomBytes(3).toString('hex');
// The sticky residential exit is shared by every setup, so a difference between rows is the browser.
const NETWORKS = {
  own: { label: 'own proxy (rotating)', proxy: process.env.PROXY_URL },
  tier1: { label: 'cdpfleet-resi Tier 1, sticky US', proxy: `cdpfleet-resi-country-us-session-c68${RUN}-lifetime-30` },
};
const SETUPS = {
  chromium: { setup: 'Chromium, headless', engine: 'chromium', type: chromium, headless: true },
  chrome: { setup: 'Google Chrome, headful', engine: 'chrome', type: chromium, headless: false },
  edge: { setup: 'Microsoft Edge, headful', engine: 'edge', type: chromium, headless: false },
  brave: { setup: 'Brave, headful', engine: 'brave', type: chromium, headless: false },
  camoufox: { setup: 'Camoufox, defaults', engine: 'camoufox', type: firefox, headless: true },
  firefox: { setup: 'Firefox, headful', engine: 'firefox', type: firefox, headless: false },
  webkit: { setup: 'WebKit, headful', engine: 'webkit', type: webkit, headless: false },
};
// Vendors' own public demo pages. Google's v3 demo asks for a score as soon as it loads and prints its
// server's verify response (with the score) on the page; hCaptcha's demo shows a checkbox widget.
const RECAPTCHA = 'https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php';
const HCAPTCHA = 'https://accounts.hcaptcha.com/demo';

// Read what the page shows; the same expressions run in every language.
const SCORE = `(() => { const m = /"score"\\s*:\\s*([0-9.]+)/.exec(document.body ? document.body.innerText : ''); return m ? Number(m[1]) : null; })()`;
const HC_WIDGET = `(() => { const f = document.querySelector('iframe[src*="hcaptcha.com"]'); return f ? (f.getBoundingClientRect().height > 0) : false; })()`;

async function launch(s, network) {
  const body = { proxy: NETWORKS[network].proxy, headless: s.headless, record: false };
  // WebKit takes an authenticated HTTP proxy through the updatable route (see the WebKit page).
  if (s.engine === 'webkit' && /^https?:/.test(body.proxy)) body.proxy_updatable = true;
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`https://starter.cdpfleet.com/${s.engine}/session`, {
      method: 'POST', headers: { 'x-api-key': KEY, 'content-type': 'application/json' }, body: JSON.stringify(body),
    });
    if (res.ok) return s.type.connect((await res.json()).wsUrl, { headers: { 'x-api-key': KEY } });
    if (![429, 503].includes(res.status) || attempt === 5) throw new Error(`launch ${res.status} ${await res.text()}`);
    await new Promise((r) => setTimeout(r, (Number(res.headers.get('retry-after')) || 5) * 1000));
  }
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// One load; then wait up to 20 s for the score the demo prints. Nothing on the page is touched.
async function recaptcha(context) {
  const tab = await context.newPage();
  try {
    await tab.goto(RECAPTCHA, { waitUntil: 'domcontentloaded', timeout: 45000 });
  } catch {
    await tab.close();
    return { outcome: 'error', score: null };
  }
  let score = null;
  for (let i = 0; i < 20 && score === null; i++) { await sleep(1000); score = await tab.evaluate(SCORE).catch(() => null); }
  await tab.close().catch(() => {});
  return { outcome: score === null ? 'no score' : 'score', score };
}

// hCaptcha only decides when someone clicks its checkbox, so a load shows whether the widget renders, nothing more.
async function hcaptcha(context) {
  const tab = await context.newPage();
  try {
    await tab.goto(HCAPTCHA, { waitUntil: 'domcontentloaded', timeout: 45000 });
  } catch {
    await tab.close();
    return { outcome: 'error' };
  }
  let shown = false;
  for (let i = 0; i < 12 && !shown; i++) { await sleep(1000); shown = await tab.evaluate(HC_WIDGET).catch(() => false); }
  await tab.close().catch(() => {});
  return { outcome: shown ? 'widget shown' : 'no widget' };
}

async function row(setupKey, network) {
  const s = SETUPS[setupKey];
  const out = { setup: s.setup, network: NETWORKS[network].label };
  let browser;
  try {
    browser = await launch(s, network);
  } catch (err) {
    return { ...out, recaptcha: { outcome: 'error', score: null }, hcaptcha: { outcome: 'error' } };
  }
  // A session the server ends (e.g. its memory limit) leaves the remaining cells as errors.
  for (const [key, fn] of [['recaptcha', recaptcha], ['hcaptcha', hcaptcha]]) {
    try {
      const context = await browser.newContext(); // a fresh context: every load is a first visit
      out[key] = await fn(context);
      await context.close();
    } catch {
      out[key] = key === 'recaptcha' ? { outcome: 'error', score: null } : { outcome: 'error' };
    }
  }
  await browser.close().catch(() => {});
  return out;
}

const rows = [];
for (const network of ['own', 'tier1']) {
  for (const setupKey of Object.keys(SETUPS)) rows.push(await row(setupKey, network));
}
console.log(JSON.stringify({ measured_at: new Date().toISOString().slice(0, 10), rows }, null, 2));

What we got

SetupNetworkreCAPTCHA v3 demo scorehCaptcha demoMeasured
Chromium, headlessown proxy (rotating)no scoreno widget2026-10-24
Google Chrome, headfulown proxy (rotating)0.9no widget2026-10-24
Microsoft Edge, headfulown proxy (rotating)0.9widget shown2026-10-24
Brave, headfulown proxy (rotating)0.9widget shown2026-10-24
Camoufox, defaultsown proxy (rotating)0.9widget shown2026-10-24
Firefox, headfulown proxy (rotating)0.9widget shown2026-10-24
WebKit, headfulown proxy (rotating)0.9widget shown2026-10-24
Chromium, headlesscdpfleet-resi Tier 1, sticky US0.9widget shown2026-10-24
Google Chrome, headfulcdpfleet-resi Tier 1, sticky US0.9widget shown2026-10-24
Microsoft Edge, headfulcdpfleet-resi Tier 1, sticky US0.9widget shown2026-10-24
Brave, headfulcdpfleet-resi Tier 1, sticky US0.9widget shown2026-10-24
Camoufox, defaultscdpfleet-resi Tier 1, sticky US0.9widget shown2026-10-24
Firefox, headfulcdpfleet-resi Tier 1, sticky US0.9widget shown2026-10-24
WebKit, headfulcdpfleet-resi Tier 1, sticky US0.9widget shown2026-10-24

From the Node.js run on 2026-10-24. 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)
{
  "measured_at": "2026-10-24",
  "rows": [
    {
      "setup": "Chromium, headless",
      "network": "own proxy (rotating)",
      "recaptcha": {
        "outcome": "no score",
        "score": null
      },
      "hcaptcha": {
        "outcome": "no widget"
      }
    },
    {
      "setup": "Google Chrome, headful",
      "network": "own proxy (rotating)",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "no widget"
      }
    },
    {
      "setup": "Microsoft Edge, headful",
      "network": "own proxy (rotating)",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "Brave, headful",
      "network": "own proxy (rotating)",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "Camoufox, defaults",
      "network": "own proxy (rotating)",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "Firefox, headful",
      "network": "own proxy (rotating)",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "WebKit, headful",
      "network": "own proxy (rotating)",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "Chromium, headless",
      "network": "cdpfleet-resi Tier 1, sticky US",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "Google Chrome, headful",
      "network": "cdpfleet-resi Tier 1, sticky US",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "Microsoft Edge, headful",
      "network": "cdpfleet-resi Tier 1, sticky US",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "Brave, headful",
      "network": "cdpfleet-resi Tier 1, sticky US",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "Camoufox, defaults",
      "network": "cdpfleet-resi Tier 1, sticky US",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "Firefox, headful",
      "network": "cdpfleet-resi Tier 1, sticky US",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    },
    {
      "setup": "WebKit, headful",
      "network": "cdpfleet-resi Tier 1, sticky US",
      "recaptcha": {
        "outcome": "score",
        "score": 0.9
      },
      "hcaptcha": {
        "outcome": "widget shown"
      }
    }
  ]
}

Takeaways