cdpfleet Docs GitHub Dashboard

Cases / #68 · 2026-10-24 · Easy

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

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