cdpfleet Docs GitHub Dashboard

Cases / #63 · 2026-10-20 · Easy

What four public bot-test pages say about five fleet browsers

bot.sannysoft.com, CreepJS, deviceandbrowserinfo.com and the fingerprint.com playground, each read for its own verdict on headless Chromium, headful Chrome, headful Patchright, Camoufox and headless WebKit. Headless Chromium fails the most checks everywhere; Camoufox is the only setup deviceandbrowserinfo never called a bot.

Chromium Google Chrome Patchright Camoufox WebKit

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

The problem

Before pointing a scraper at a protected site, people run their browser past public bot-test pages to see what gives it away. Each page checks different things and reports in its own way: a table of passed and failed checks, a headless percentage, a yes/no verdict with reasons. This case reads every page's own verdict for five setups on the fleet, so you can see which signals each one keys on. It records what the pages report on one day, not how a particular protected site will treat you.

What we used, and why

WhatWhy
bot.sannysoft.comA table of classic headless and automation checks (user agent, navigator.webdriver, the chrome object, plugins, permissions, WebGL). The program counts the rows marked failed and keeps their names.
CreepJS (abrahamjuliot.github.io/creepjs)Scores how much the browser looks headless ("like headless", "headless") and how much it looks like a stealth tool ("stealth"), as percentages.
deviceandbrowserinfo.com/are_you_a_botA yes/no verdict (isBot) with the individual signals behind it, such as a bot user agent, CDP automation, webdriver true.
demo.fingerprint.com/playgroundThe vendor's playground: a bot verdict ("Not detected", or "bad bot" with a type) and a suspect score. Only those two fields are read.
page.waitForFunction()Each page runs its checks after load; the program waits until the verdict is on the page before reading it.
One context per setup, one load per pageThe pages see a fresh browser each time, and no page is loaded more than once per setup.

How it works

  1. For each of five setups, launch a session through the same proxy and open a fresh context.
  2. Open each test page in turn and wait until it shows its result.
  3. Read the page's own verdict from its DOM, with the same expression in every language.
  4. Print one row per setup with the four verdicts.

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 (an http(s) or socks5 proxy; one page load per test page per setup)
import { chromium, firefox, webkit } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const PROXY = process.env.PROXY_URL;

const SETUPS = [
  { setup: 'Chromium, headless', engine: 'chromium', family: 'chromium', body: { headless: 'new' } },
  { setup: 'Google Chrome, headful', engine: 'chrome', family: 'chromium', body: { headless: false } },
  { setup: 'Patchright, headful', engine: 'patchright', family: 'chromium', body: { headless: false } },
  { setup: 'Camoufox, defaults', engine: 'camoufox', family: 'firefox', body: { headless: true } },
  { setup: 'WebKit, headless', engine: 'webkit', family: 'webkit', body: { headless: true } },
];
const TYPES = { chromium, firefox, webkit };

// Each test page's own verdict, read from its DOM. The same expressions run in every language;
// "ready" is polled until the page has finished its checks.
const PAGES = [
  {
    key: 'sannysoft', url: 'https://bot.sannysoft.com/',
    ready: "document.querySelectorAll('td.passed, td.failed').length > 10",
    read: "(() => { const rows = Array.from(document.querySelectorAll('tr')).filter((tr) => tr.querySelector('td.passed, td.failed, td.warn')); const failed = rows.filter((tr) => tr.querySelector('td.failed')).map((tr) => tr.cells[0].innerText.replace(/\\s+/g, ' ').trim()); return { checks: rows.length, failed: failed.length, failed_names: failed }; })()",
  },
  {
    key: 'creepjs', url: 'https://abrahamjuliot.github.io/creepjs/',
    ready: "/\\d+% stealth/.test(document.body.innerText)",
    read: "(() => { const t = document.body.innerText.replace(/\\s+/g, ' '); const m = /(\\d+)% like headless[\\s\\S]*?(\\d+)% headless:[\\s\\S]*?(\\d+)% stealth/.exec(t); return m ? { like_headless_pct: +m[1], headless_pct: +m[2], stealth_pct: +m[3] } : { like_headless_pct: null, headless_pct: null, stealth_pct: null }; })()",
  },
  {
    key: 'deviceandbrowserinfo', url: 'https://deviceandbrowserinfo.com/are_you_a_bot',
    ready: "document.body.innerText.indexOf('\"isBot\"') >= 0",
    read: "(() => { const t = document.body.innerText; const s = t.indexOf('{', t.indexOf('\"isBot\"') - 10); let depth = 0, e = s; for (; e < t.length; e++) { if (t[e] === '{') depth++; if (t[e] === '}' && --depth === 0) break; } try { const j = JSON.parse(t.slice(s, e + 1)); return { is_bot: j.isBot, signals: Object.keys(j.details || {}).filter((k) => j.details[k] === true) }; } catch (err) { return { is_bot: null, signals: [] }; } })()",
  },
  {
    key: 'fingerprint', url: 'https://demo.fingerprint.com/playground',
    ready: "/Suspect Score \\d+/.test(document.body.innerText.replace(/\\s+/g, ' '))",
    // Only the bot verdict and the suspect score are read (the page also names proxy networks).
    read: "(() => { const t = document.body.innerText.replace(/\\s+/g, ' '); const bot = /\\bBot (.+?) VPN /.exec(t); const score = /Suspect Score (\\d+)/.exec(t); return { bot: bot ? bot[1].replace(/[^\\x20-\\x7e]/g, '').replace(/\\s+/g, ' ').trim() : null, suspect_score: score ? +score[1] : null }; })()",
  },
];

// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async function launch(engine, body) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`https://starter.cdpfleet.com/${engine}/session`, {
      method: 'POST',
      headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
      body: JSON.stringify({ proxy: PROXY, record: false, ...body }),
    });
    if (res.ok) return res.json();
    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 results = [];
for (const s of SETUPS) {
  const session = await launch(s.engine, s.body);
  const browser = await TYPES[s.family].connect(session.wsUrl, { headers: { 'x-api-key': KEY } });
  const row = { setup: s.setup, engine: s.engine };
  try {
    const page = await (await browser.newContext()).newPage();
    for (const p of PAGES) {
      try {
        await page.goto(p.url, { timeout: 60000 });
        await page.waitForFunction(p.ready, null, { timeout: 45000 });
        row[p.key] = await page.evaluate(p.read);
      } catch (err) {
        row[p.key] = { error: err.message.split('\n')[0].slice(0, 120) };
      }
    }
  } finally {
    await browser.close();
  }
  results.push(row);
}
console.log(JSON.stringify(results, null, 2));

What we got

Setupsannysoft: failed checksCreepJS: like headless / headless / stealthdeviceandbrowserinfofingerprint.com
Chromium, headless11 of 31 (User Agent (Old), Chrome (New), Permissions (New), Plugins Length (Old), Plugins is of type PluginArray, WebGL Renderer, HEADCHR_UA, HEADCHR_CHROME_OBJ, HEADCHR_PERMISSIONS, HEADCHR_IFRAME, CHR_MEMORY)94% / 67% / 0%bot (hasBotUserAgent, hasInconsistentTimingResolution, isAutomatedWithCDP, isAutomatedWithCDPInWebWorker, hasSuspiciousWeakSignals)You are a bad bot (type: headless_chrome), suspect score 23
Google Chrome, headful1 of 31 (WebGL Renderer)50% / 0% / 0%no result (page load failed)You are a bad bot (type: chromium_automation), suspect score 43
Patchright, headful1 of 31 (WebGL Renderer)50% / 0% / 0%bot (hasSuspiciousWeakSignals)You are a bad bot (type: chromium_automation), suspect score 31
Camoufox, defaults1 of 31 (Chrome (New))13% / 0% / 0%not a botNot detected, suspect score 9
WebKit, headless2 of 31 (WebDriver (New), Chrome (New))19% / 33% / 0%no result (page load failed)You are a bad bot (type: webdriver), suspect score 33

From the Node.js run on 2026-10-20. 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)
[
  {
    "setup": "Chromium, headless",
    "engine": "chromium",
    "sannysoft": {
      "checks": 31,
      "failed": 11,
      "failed_names": [
        "User Agent (Old)",
        "Chrome (New)",
        "Permissions (New)",
        "Plugins Length (Old)",
        "Plugins is of type PluginArray",
        "WebGL Renderer",
        "HEADCHR_UA",
        "HEADCHR_CHROME_OBJ",
        "HEADCHR_PERMISSIONS",
        "HEADCHR_IFRAME",
        "CHR_MEMORY"
      ]
    },
    "creepjs": {
      "like_headless_pct": 94,
      "headless_pct": 67,
      "stealth_pct": 0
    },
    "deviceandbrowserinfo": {
      "is_bot": true,
      "signals": [
        "hasBotUserAgent",
        "hasInconsistentTimingResolution",
        "isAutomatedWithCDP",
        "isAutomatedWithCDPInWebWorker",
        "hasSuspiciousWeakSignals"
      ]
    },
    "fingerprint": {
      "bot": "You are a bad bot (type: headless_chrome)",
      "suspect_score": 23
    }
  },
  {
    "setup": "Google Chrome, headful",
    "engine": "chrome",
    "sannysoft": {
      "checks": 31,
      "failed": 1,
      "failed_names": [
        "WebGL Renderer"
      ]
    },
    "creepjs": {
      "like_headless_pct": 50,
      "headless_pct": 0,
      "stealth_pct": 0
    },
    "deviceandbrowserinfo": {
      "error": "page.goto: Timeout 60000ms exceeded."
    },
    "fingerprint": {
      "bot": "You are a bad bot (type: chromium_automation)",
      "suspect_score": 43
    }
  },
  {
    "setup": "Patchright, headful",
    "engine": "patchright",
    "sannysoft": {
      "checks": 31,
      "failed": 1,
      "failed_names": [
        "WebGL Renderer"
      ]
    },
    "creepjs": {
      "like_headless_pct": 50,
      "headless_pct": 0,
      "stealth_pct": 0
    },
    "deviceandbrowserinfo": {
      "is_bot": true,
      "signals": [
        "hasSuspiciousWeakSignals"
      ]
    },
    "fingerprint": {
      "bot": "You are a bad bot (type: chromium_automation)",
      "suspect_score": 31
    }
  },
  {
    "setup": "Camoufox, defaults",
    "engine": "camoufox",
    "sannysoft": {
      "checks": 31,
      "failed": 1,
      "failed_names": [
        "Chrome (New)"
      ]
    },
    "creepjs": {
      "like_headless_pct": 13,
      "headless_pct": 0,
      "stealth_pct": 0
    },
    "deviceandbrowserinfo": {
      "is_bot": false,
      "signals": []
    },
    "fingerprint": {
      "bot": "Not detected",
      "suspect_score": 9
    }
  },
  {
    "setup": "WebKit, headless",
    "engine": "webkit",
    "sannysoft": {
      "checks": 31,
      "failed": 2,
      "failed_names": [
        "WebDriver (New)",
        "Chrome (New)"
      ]
    },
    "creepjs": {
      "like_headless_pct": 19,
      "headless_pct": 33,
      "stealth_pct": 0
    },
    "deviceandbrowserinfo": {
      "error": "page.goto: Timeout 60000ms exceeded."
    },
    "fingerprint": {
      "bot": "You are a bad bot (type: webdriver)",
      "suspect_score": 33
    }
  }
]

Takeaways