cdpfleet Docs GitHub Dashboard

Cases / #70 · 2026-10-26 · Medium

Cloudflare on real sites: twelve browsers, three sites, one exit each

Three real sites behind Cloudflare, chosen from our sweep as easy, medium and hard, loaded once by each of twelve browsers and by a returning Camoufox profile, all through one sticky residential exit per site. The outcome per cell, dated 2026-10-26.

Google Chrome Microsoft Edge Brave Opera Yandex Browser Naver Whale Chromium Patchright Firefox WebKit Camoufox

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

The problem

Cloudflare's own test pages (case 64) show how its checks behave, but a real site sets its own rules. This case takes three real sites behind Cloudflare from our sweep of production sites, one that let headless Chrome in (easy), one that only Camoufox passed (medium) and one that stopped Camoufox the day before (hard), and loads each once with every browser the fleet offers, plus a returning Camoufox profile, through one sticky residential exit per site. The sites aren't named: they stand for a kind of site, and their rules can change any day. It is a dated measurement, not an evasion guide; for the signals a page can read, see What a page can read about your browser, and for the whole picture /bot-protection.

What we used, and why

WhatWhy
Twelve browsersChrome headful and headless, Edge, Brave, Opera, Yandex, Whale, Chromium headless and Patchright (all Chromium), Firefox, WebKit, and Camoufox (based on Firefox source; not affiliated with Mozilla). Rebrowser needs a Playwright 1.52 client, so it's left out of a program written for 1.60.
proxy: cdpfleet-resi-country-us-session-<id>One sticky residential exit per site, shared by every setup on that site, so a difference between rows is the browser.
One session at a time per siteEach site's setups run one after another; the three sites run side by side.
page.evaluate() after 8 sThe title and opening text classify the page: Cloudflare's interstitial is challenge, a block page or an error status with a short page is blocked, anything else content. Nothing is clicked.
profileA returning visitor: Camoufox loads the site once with a new profile, the profile is saved, and a second session with it loads the site again.
LAB_SITESThe three URLs come from the environment, so the published programs don't name the sites; set your own to repeat the measurement.

How it works

  1. For each site, take one sticky residential exit and, one session at a time, open each of the twelve setups, load the page once and wait 8 s.
  2. Classify what the page shows and record the HTTP status of its first response; a network error gets one retry.
  3. Then load the site with Camoufox and a new profile, wait for the profile to be saved, load it again with the same profile and record the second visit. Delete the profile.

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, LAB_SITES (comma-separated URLs of the pages to test; one load per page per setup, nothing clicked)
import { randomBytes } from 'node:crypto';
import { chromium, firefox, webkit } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
const RUN = randomBytes(3).toString('hex');
const SITES = process.env.LAB_SITES.split(',').map((url, i) => ({ site: String.fromCharCode(65 + i), url }));
// Every browser the fleet offers that a Playwright 1.60 client can drive (Rebrowser needs 1.52, so it's left out).
const SETUPS = [
  ['chrome_headful', 'Chrome, headful', 'chrome', chromium, false],
  ['chrome_headless', 'Chrome, headless', 'chrome', chromium, 'new'],
  ['edge', 'Edge, headful', 'edge', chromium, false],
  ['brave', 'Brave, headful', 'brave', chromium, false],
  ['opera', 'Opera, headful', 'opera', chromium, false],
  ['yandex', 'Yandex, headful', 'yandex', chromium, false],
  ['whale', 'Whale, headful', 'whale', chromium, false],
  ['chromium_headless', 'Chromium, headless', 'chromium', chromium, true],
  ['patchright', 'Patchright, headful', 'patchright', chromium, false],
  ['firefox', 'Firefox, headful', 'firefox', firefox, false],
  ['webkit', 'WebKit, headful', 'webkit', webkit, false],
  ['camoufox', 'Camoufox, headful', 'camoufox', firefox, false],
];

// What the page shows after it loaded and had 8 s to run its checks, in the lab's words.
const STATE = `(() => {
  const t = (document.title + ' ' + (document.body ? document.body.innerText : '').slice(0, 600)).replace(/\\s+/g, ' ');
  const len = document.body ? document.body.innerText.length : 0;
  if (/sorry, you have been blocked|attention required|error 1020|access denied/i.test(t) && len < 4000) return 'blocked';
  if (/just a moment|performing security verification|verify you are human|checking your browser|checking if the site connection is secure/i.test(t) && len < 4000) return 'challenge';
  return 'content';
})()`;

async function launch(engine, headless, proxy, profile) {
  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, headless, record: false, ...(profile ? { profile } : {}) }),
    });
    if (res.ok) return (await res.json()).wsUrl;
    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));
  }
}

// One session, one load: the status of the page's own response, then its state 8 s later.
async function visit(setup, url, proxy, profile) {
  const [, , engine, type, headless] = setup;
  const browser = await type.connect(await launch(engine, headless, proxy, profile), { headers: { 'x-api-key': KEY } });
  try {
    const context = profile ? browser.contexts()[0] : await browser.newContext();
    const tab = await context.newPage();
    let status = 0;
    try {
      status = (await tab.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 }))?.status() ?? 0;
    } catch {
      return { outcome: 'error', status: 0 };
    }
    await tab.waitForTimeout(8000);
    let outcome = await tab.evaluate(STATE).catch(() => 'error');
    if (outcome === 'content' && status >= 400) outcome = 'blocked';
    return { outcome, status };
  } finally {
    await browser.close();
  }
}

async function api(method, path) {
  const res = await fetch(`https://cdpfleet.com${path}`, { method, headers: { 'x-api-key': KEY } });
  return res.json().catch(() => ({}));
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// One sticky residential exit per site; its setups run one after another (one session at a time per
// site), and the sites run side by side. A network error gets one more try.
async function site({ site, url }) {
  const proxy = `cdpfleet-resi-country-us-session-c70${RUN}${site}-lifetime-30`;
  const results = {};
  for (const setup of SETUPS) {
    let r = await visit(setup, url, proxy).catch(() => ({ outcome: 'error', status: 0 }));
    if (r.outcome === 'error') r = await visit(setup, url, proxy).catch(() => ({ outcome: 'error', status: 0 }));
    results[setup[0]] = r;
  }
  // A returning visitor: Camoufox with a profile, a first visit and then a second session.
  const name = `case70-${RUN}-${site}`;
  const camoufox = SETUPS.find((s) => s[0] === 'camoufox');
  await visit(camoufox, url, proxy, name).catch(() => null);
  for (let j = 0; j < 60 && (await api('GET', '/v1/me/profiles')).profiles?.find((p) => p.name === name)?.in_use !== false; j++) await sleep(2000);
  results.camoufox_returning = await visit(camoufox, url, proxy, name).catch(() => ({ outcome: 'error', status: 0 }));
  await api('DELETE', `/v1/me/profiles/${name}`);
  return { site, results };
}

const sites = await Promise.all(SITES.map(site));
console.log(JSON.stringify({ measured_at: new Date().toISOString().slice(0, 10), setups: Object.fromEntries([...SETUPS.map((s) => [s[0], s[1]]), ['camoufox_returning', 'Camoufox, returning profile (2nd visit)']]), sites }, null, 2));

What we got

SetupSite A, a fashion retailerSite B, a design-software siteSite C, a shipping companyMeasured
Chrome, headfulcontent (200)content (200)content (200)2026-10-26
Chrome, headlesscontent (200)challenge (403)challenge (403)2026-10-26
Edge, headfulcontent (200)content (200)content (200)2026-10-26
Brave, headfulcontent (200)content (200)content (200)2026-10-26
Opera, headfulcontent (200)content (200)content (200)2026-10-26
Yandex, headfulcontent (200)content (200)content (200)2026-10-26
Whale, headfulcontent (200)content (200)content (200)2026-10-26
Chromium, headlesscontent (200)challenge (403)challenge (403)2026-10-26
Patchright, headfulcontent (200)content (200)challenge (403)2026-10-26
Firefox, headfulcontent (200)content (200)challenge (403)2026-10-26
WebKit, headfulcontent (200)challenge (403)challenge (403)2026-10-26
Camoufox, headfulcontent (200)content (200)content (200)2026-10-26
Camoufox, returning profile (2nd visit)content (200)content (200)content (200)2026-10-26

From the Node.js run on 2026-10-26. 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-26",
  "setups": {
    "chrome_headful": "Chrome, headful",
    "chrome_headless": "Chrome, headless",
    "edge": "Edge, headful",
    "brave": "Brave, headful",
    "opera": "Opera, headful",
    "yandex": "Yandex, headful",
    "whale": "Whale, headful",
    "chromium_headless": "Chromium, headless",
    "patchright": "Patchright, headful",
    "firefox": "Firefox, headful",
    "webkit": "WebKit, headful",
    "camoufox": "Camoufox, headful",
    "camoufox_returning": "Camoufox, returning profile (2nd visit)"
  },
  "sites": [
    {
      "site": "A",
      "results": {
        "chrome_headful": {
          "outcome": "content",
          "status": 200
        },
        "chrome_headless": {
          "outcome": "content",
          "status": 200
        },
        "edge": {
          "outcome": "content",
          "status": 200
        },
        "brave": {
          "outcome": "content",
          "status": 200
        },
        "opera": {
          "outcome": "content",
          "status": 200
        },
        "yandex": {
          "outcome": "content",
          "status": 200
        },
        "whale": {
          "outcome": "content",
          "status": 200
        },
        "chromium_headless": {
          "outcome": "content",
          "status": 200
        },
        "patchright": {
          "outcome": "content",
          "status": 200
        },
        "firefox": {
          "outcome": "content",
          "status": 200
        },
        "webkit": {
          "outcome": "content",
          "status": 200
        },
        "camoufox": {
          "outcome": "content",
          "status": 200
        },
        "camoufox_returning": {
          "outcome": "content",
          "status": 200
        }
      }
    },
    {
      "site": "B",
      "results": {
        "chrome_headful": {
          "outcome": "content",
          "status": 200
        },
        "chrome_headless": {
          "outcome": "challenge",
          "status": 403
        },
        "edge": {
          "outcome": "content",
          "status": 200
        },
        "brave": {
          "outcome": "content",
          "status": 200
        },
        "opera": {
          "outcome": "content",
          "status": 200
        },
        "yandex": {
          "outcome": "content",
          "status": 200
        },
        "whale": {
          "outcome": "content",
          "status": 200
        },
        "chromium_headless": {
          "outcome": "challenge",
          "status": 403
        },
        "patchright": {
          "outcome": "content",
          "status": 200
        },
        "firefox": {
          "outcome": "content",
          "status": 200
        },
        "webkit": {
          "outcome": "challenge",
          "status": 403
        },
        "camoufox": {
          "outcome": "content",
          "status": 200
        },
        "camoufox_returning": {
          "outcome": "content",
          "status": 200
        }
      }
    },
    {
      "site": "C",
      "results": {
        "chrome_headful": {
          "outcome": "content",
          "status": 200
        },
        "chrome_headless": {
          "outcome": "challenge",
          "status": 403
        },
        "edge": {
          "outcome": "content",
          "status": 200
        },
        "brave": {
          "outcome": "content",
          "status": 200
        },
        "opera": {
          "outcome": "content",
          "status": 200
        },
        "yandex": {
          "outcome": "content",
          "status": 200
        },
        "whale": {
          "outcome": "content",
          "status": 200
        },
        "chromium_headless": {
          "outcome": "challenge",
          "status": 403
        },
        "patchright": {
          "outcome": "challenge",
          "status": 403
        },
        "firefox": {
          "outcome": "challenge",
          "status": 403
        },
        "webkit": {
          "outcome": "challenge",
          "status": 403
        },
        "camoufox": {
          "outcome": "content",
          "status": 200
        },
        "camoufox_returning": {
          "outcome": "content",
          "status": 200
        }
      }
    }
  ]
}

Takeaways