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
| What | Why |
|---|---|
| Twelve browsers | Chrome 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 site | Each site's setups run one after another; the three sites run side by side. |
page.evaluate() after 8 s | The 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. |
profile | A 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_SITES | The 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
- 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.
- Classify what the page shows and record the HTTP status of its first response; a network error gets one retry.
- 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:
CDPFLEET_API_KEY— your API key (dashboard → API keys)
// 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));
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, LAB_SITES (comma-separated URLs of the pages to test; one load per page per setup, nothing clicked)
import asyncio
import json
import os
import secrets
from datetime import datetime, timezone
import requests
from playwright.async_api import async_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
RUN = secrets.token_hex(3)
SITES = [{"site": chr(65 + i), "url": u} for i, u in enumerate(os.environ["LAB_SITES"].split(","))]
# Every browser the fleet offers that a Playwright 1.60 client can drive (Rebrowser needs 1.52, so it's left out).
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.
STATE = r"""(() => {
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';
})()"""
def api(method, path):
try:
return requests.request(method, f"https://cdpfleet.com{path}", headers={"x-api-key": KEY}, timeout=30).json()
except Exception:
return {}
async def launch(engine, headless, proxy, profile=None):
body = {"proxy": proxy, "headless": headless, "record": False, **({"profile": profile} if profile else {})}
for attempt in range(1, 6):
res = await asyncio.to_thread(requests.post, f"https://starter.cdpfleet.com/{engine}/session",
headers={"x-api-key": KEY}, json=body, timeout=120)
if res.ok:
return res.json()["wsUrl"]
if res.status_code not in (429, 503) or attempt == 5:
raise RuntimeError(f"launch {res.status_code} {res.text}")
await asyncio.sleep(int(res.headers.get("retry-after") or 5))
# One session, one load: the status of the page's own response, then its state 8 s later.
async def visit(pw, setup, url, proxy, profile=None):
_, _, engine, kind, headless = setup
browser = await getattr(pw, kind).connect(await launch(engine, headless, proxy, profile), headers={"x-api-key": KEY})
try:
context = browser.contexts[0] if profile else await browser.new_context()
tab = await context.new_page()
try:
res = await tab.goto(url, wait_until="domcontentloaded", timeout=45000)
status = res.status if res else 0
except Exception:
return {"outcome": "error", "status": 0}
await tab.wait_for_timeout(8000)
try:
outcome = await tab.evaluate(STATE)
except Exception:
outcome = "error"
if outcome == "content" and status >= 400:
outcome = "blocked"
return {"outcome": outcome, "status": status}
finally:
await browser.close()
async def safe(pw, setup, url, proxy, profile=None):
try:
return await visit(pw, setup, url, proxy, profile)
except Exception:
return {"outcome": "error", "status": 0}
# 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 def site(pw, s):
proxy = f"cdpfleet-resi-country-us-session-c70{RUN}{s['site']}-lifetime-30"
results = {}
for setup in SETUPS:
r = await safe(pw, setup, s["url"], proxy)
if r["outcome"] == "error":
r = await safe(pw, setup, s["url"], proxy)
results[setup[0]] = r
# A returning visitor: Camoufox with a profile, a first visit and then a second session.
name = f"case70-{RUN}-{s['site']}"
camoufox = next(x for x in SETUPS if x[0] == "camoufox")
await safe(pw, camoufox, s["url"], proxy, name)
for _ in range(60):
p = next((p for p in api("GET", "/v1/me/profiles").get("profiles", []) if p["name"] == name), None)
if p and p.get("in_use") is False:
break
await asyncio.sleep(2)
results["camoufox_returning"] = await safe(pw, camoufox, s["url"], proxy, name)
api("DELETE", f"/v1/me/profiles/{name}")
return {"site": s["site"], "results": results}
async def main():
async with async_playwright() as pw:
sites = await asyncio.gather(*(site(pw, s) for s in SITES))
setups = {x[0]: x[1] for x in SETUPS}
setups["camoufox_returning"] = "Camoufox, returning profile (2nd visit)"
print(json.dumps({"measured_at": datetime.now(timezone.utc).date().isoformat(), "setups": setups, "sites": sites}, indent=2))
asyncio.run(main())
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1.
// env: CDPFLEET_API_KEY, LAB_SITES (comma-separated URLs of the pages to test; one load per page per setup, nothing clicked)
import com.google.gson.*;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.WaitUntilState;
import java.net.URI;
import java.net.http.*;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().serializeNulls().create();
static final HttpClient HTTP = HttpClient.newHttpClient();
static final String RUN = String.format("%06x", new SecureRandom().nextInt(1 << 24));
record Setup(String id, String name, String engine, String type, Object headless) {}
// Every browser the fleet offers that a Playwright 1.60 client can drive (Rebrowser needs 1.52, so it's left out).
static final List<Setup> SETUPS = List.of(
new Setup("chrome_headful", "Chrome, headful", "chrome", "chromium", false),
new Setup("chrome_headless", "Chrome, headless", "chrome", "chromium", "new"),
new Setup("edge", "Edge, headful", "edge", "chromium", false),
new Setup("brave", "Brave, headful", "brave", "chromium", false),
new Setup("opera", "Opera, headful", "opera", "chromium", false),
new Setup("yandex", "Yandex, headful", "yandex", "chromium", false),
new Setup("whale", "Whale, headful", "whale", "chromium", false),
new Setup("chromium_headless", "Chromium, headless", "chromium", "chromium", true),
new Setup("patchright", "Patchright, headful", "patchright", "chromium", false),
new Setup("firefox", "Firefox, headful", "firefox", "firefox", false),
new Setup("webkit", "WebKit, headful", "webkit", "webkit", false),
new Setup("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.
static final String 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';
})()""";
static JsonObject api(String method, String path) {
try {
HttpRequest req = HttpRequest.newBuilder(URI.create("https://cdpfleet.com" + path)).header("x-api-key", KEY)
.method(method, HttpRequest.BodyPublishers.noBody()).build();
return JsonParser.parseString(HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body()).getAsJsonObject();
} catch (Exception e) {
return new JsonObject();
}
}
static String launch(String engine, Object headless, String proxy, String profile) throws Exception {
Map<String, Object> body = new LinkedHashMap<>(Map.of("proxy", proxy, "headless", headless, "record", false));
if (profile != null) body.put("profile", profile);
for (int attempt = 1; ; attempt++) {
HttpRequest req = HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/" + engine + "/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(body))).build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() == 200) return JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
if ((res.statusCode() != 429 && res.statusCode() != 503) || attempt == 5) throw new RuntimeException("launch " + res.statusCode() + " " + res.body());
Thread.sleep(Long.parseLong(res.headers().firstValue("retry-after").orElse("5")) * 1000);
}
}
static Map<String, Object> cell(String outcome, int status) { return new LinkedHashMap<>(Map.of("outcome", outcome, "status", status)); }
// One session, one load: the status of the page's own response, then its state 8 s later.
static Map<String, Object> visit(Playwright pw, Setup s, String url, String proxy, String profile) {
try {
BrowserType type = switch (s.type()) { case "firefox" -> pw.firefox(); case "webkit" -> pw.webkit(); default -> pw.chromium(); };
Browser browser = type.connect(launch(s.engine(), s.headless(), proxy, profile),
new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
BrowserContext context = profile != null ? browser.contexts().get(0) : browser.newContext();
Page tab = context.newPage();
int status;
try {
Response res = tab.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED).setTimeout(45000));
status = res != null ? res.status() : 0;
} catch (PlaywrightException e) {
return cell("error", 0);
}
tab.waitForTimeout(8000);
String outcome;
try { outcome = (String) tab.evaluate(STATE); } catch (PlaywrightException e) { outcome = "error"; }
if ("content".equals(outcome) && status >= 400) outcome = "blocked";
return cell(outcome, status);
} finally {
browser.close();
}
} catch (Exception e) {
return cell("error", 0);
}
}
// 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.
static Map<String, Object> site(String label, String url) throws Exception {
try (Playwright pw = Playwright.create()) {
String proxy = "cdpfleet-resi-country-us-session-c70" + RUN + label + "-lifetime-30";
Map<String, Object> results = new LinkedHashMap<>();
for (Setup s : SETUPS) {
Map<String, Object> r = visit(pw, s, url, proxy, null);
if ("error".equals(r.get("outcome"))) r = visit(pw, s, url, proxy, null);
results.put(s.id(), r);
}
// A returning visitor: Camoufox with a profile, a first visit and then a second session.
String name = "case70-" + RUN + "-" + label;
Setup camoufox = SETUPS.get(SETUPS.size() - 1);
visit(pw, camoufox, url, proxy, name);
for (int i = 0; i < 60; i++) {
boolean free = false;
JsonObject list = api("GET", "/v1/me/profiles");
if (list.has("profiles")) for (JsonElement p : list.getAsJsonArray("profiles")) {
JsonObject o = p.getAsJsonObject();
if (name.equals(o.get("name").getAsString()) && o.has("in_use") && !o.get("in_use").getAsBoolean()) free = true;
}
if (free) break;
Thread.sleep(2000);
}
results.put("camoufox_returning", visit(pw, camoufox, url, proxy, name));
api("DELETE", "/v1/me/profiles/" + name);
return new LinkedHashMap<>(Map.of("site", label, "results", results));
}
}
public static void main(String[] args) throws Exception {
String[] urls = System.getenv("LAB_SITES").split(",");
ExecutorService pool = Executors.newFixedThreadPool(urls.length);
List<Future<Map<String, Object>>> futures = new ArrayList<>();
for (int i = 0; i < urls.length; i++) {
String label = String.valueOf((char) ('A' + i)), url = urls[i];
futures.add(pool.submit(() -> site(label, url)));
}
List<Map<String, Object>> sites = new ArrayList<>();
for (Future<Map<String, Object>> f : futures) sites.add(f.get());
pool.shutdown();
Map<String, String> names = new LinkedHashMap<>();
for (Setup s : SETUPS) names.put(s.id(), s.name());
names.put("camoufox_returning", "Camoufox, returning profile (2nd visit)");
Map<String, Object> out = new LinkedHashMap<>();
out.put("measured_at", Instant.now().toString().substring(0, 10));
out.put("setups", names);
out.put("sites", sites);
System.out.println(GSON.toJson(out));
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, LAB_SITES (comma-separated URLs of the pages to test; one load per page per setup, nothing clicked)
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
var run = Convert.ToHexString(RandomNumberGenerator.GetBytes(3)).ToLowerInvariant();
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
using var playwright = await Playwright.CreateAsync();
var sites = Environment.GetEnvironmentVariable("LAB_SITES")!.Split(',').Select((u, i) => (Site: ((char)('A' + i)).ToString(), Url: u)).ToArray();
// Every browser the fleet offers that a Playwright 1.60 client can drive (Rebrowser needs 1.52, so it's left out).
var setups = new (string Id, string Name, string Engine, IBrowserType Type, JsonNode Headless)[]
{
("chrome_headful", "Chrome, headful", "chrome", playwright.Chromium, false),
("chrome_headless", "Chrome, headless", "chrome", playwright.Chromium, "new"),
("edge", "Edge, headful", "edge", playwright.Chromium, false),
("brave", "Brave, headful", "brave", playwright.Chromium, false),
("opera", "Opera, headful", "opera", playwright.Chromium, false),
("yandex", "Yandex, headful", "yandex", playwright.Chromium, false),
("whale", "Whale, headful", "whale", playwright.Chromium, false),
("chromium_headless", "Chromium, headless", "chromium", playwright.Chromium, true),
("patchright", "Patchright, headful", "patchright", playwright.Chromium, false),
("firefox", "Firefox, headful", "firefox", playwright.Firefox, false),
("webkit", "WebKit, headful", "webkit", playwright.Webkit, false),
("camoufox", "Camoufox, headful", "camoufox", playwright.Firefox, false),
};
// What the page shows after it loaded and had 8 s to run its checks, in the lab's words.
const string 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 Task<string> Launch(string engine, JsonNode headless, string proxy, string? profile)
{
var body = new JsonObject { ["proxy"] = proxy, ["headless"] = headless.DeepClone(), ["record"] = false };
if (profile != null) body["profile"] = profile;
for (var attempt = 1; ; attempt++)
{
var res = await http.PostAsync($"https://starter.cdpfleet.com/{engine}/session", JsonContent.Create(body));
if (res.IsSuccessStatusCode) return (await res.Content.ReadFromJsonAsync<JsonObject>())!["wsUrl"]!.GetValue<string>();
if (((int)res.StatusCode != 429 && (int)res.StatusCode != 503) || attempt == 5)
throw new Exception($"launch {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
await Task.Delay((int)(res.Headers.RetryAfter?.Delta?.TotalSeconds ?? 5) * 1000);
}
}
JsonObject Cell(string outcome, int status) => new() { ["outcome"] = outcome, ["status"] = status };
// One session, one load: the status of the page's own response, then its state 8 s later.
async Task<JsonObject> Visit((string Id, string Name, string Engine, IBrowserType Type, JsonNode Headless) s, string url, string proxy, string? profile = null)
{
try
{
var browser = await s.Type.ConnectAsync(await Launch(s.Engine, s.Headless, proxy, profile),
new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
var context = profile != null ? browser.Contexts[0] : await browser.NewContextAsync();
var tab = await context.NewPageAsync();
int status;
try
{
var res = await tab.GotoAsync(url, new() { WaitUntil = WaitUntilState.DOMContentLoaded, Timeout = 45000 });
status = res?.Status ?? 0;
}
catch (PlaywrightException) { return Cell("error", 0); }
await tab.WaitForTimeoutAsync(8000);
string outcome;
try { outcome = await tab.EvaluateAsync<string>(State); } catch (PlaywrightException) { outcome = "error"; }
if (outcome == "content" && status >= 400) outcome = "blocked";
return Cell(outcome, status);
}
finally { await browser.CloseAsync(); }
}
catch (Exception) { return Cell("error", 0); }
}
// 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 Task<JsonObject> Site(string label, string url)
{
var proxy = $"cdpfleet-resi-country-us-session-c70{run}{label}-lifetime-30";
var results = new JsonObject();
foreach (var s in setups)
{
var r = await Visit(s, url, proxy);
if (r["outcome"]!.GetValue<string>() == "error") r = await Visit(s, url, proxy);
results[s.Id] = r;
}
// A returning visitor: Camoufox with a profile, a first visit and then a second session.
var name = $"case70-{run}-{label}";
var camoufox = setups[^1];
await Visit(camoufox, url, proxy, name);
for (var i = 0; i < 60; i++)
{
var list = await http.GetFromJsonAsync<JsonObject>("https://cdpfleet.com/v1/me/profiles");
var p = list?["profiles"]?.AsArray().FirstOrDefault(x => x?["name"]?.GetValue<string>() == name);
if (p?["in_use"]?.GetValue<bool>() == false) break;
await Task.Delay(2000);
}
results["camoufox_returning"] = await Visit(camoufox, url, proxy, name);
await http.DeleteAsync($"https://cdpfleet.com/v1/me/profiles/{name}");
return new JsonObject { ["site"] = label, ["results"] = results };
}
var done = await Task.WhenAll(sites.Select(x => Site(x.Site, x.Url)));
var names = new JsonObject();
foreach (var s in setups) names[s.Id] = s.Name;
names["camoufox_returning"] = "Camoufox, returning profile (2nd visit)";
var output = new JsonObject
{
["measured_at"] = DateTime.UtcNow.ToString("yyyy-MM-dd"),
["setups"] = names,
["sites"] = new JsonArray(done.Select(d => (JsonNode)d).ToArray()),
};
Console.WriteLine(output.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, LAB_SITES (comma-separated URLs of the pages to test; one load per page per setup, nothing clicked)
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_KEY")
var runID = func() string { b := make([]byte, 3); rand.Read(b); return hex.EncodeToString(b) }()
type setup struct {
ID, Name, Engine, Type string
Headless any // false, true or "new"
}
// Every browser the fleet offers that a Playwright 1.60 client can drive (Rebrowser needs 1.52, so it's left out).
var setups = []setup{
{"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';
})()`
type cell struct {
Outcome string `json:"outcome"`
Status int `json:"status"`
}
func api(method, path string) map[string]any {
req, _ := http.NewRequest(method, "https://cdpfleet.com"+path, nil)
req.Header.Set("x-api-key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
return map[string]any{}
}
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
return out
}
func launch(engine string, headless any, proxy, profile string) (string, error) {
body := map[string]any{"proxy": proxy, "headless": headless, "record": false}
if profile != "" {
body["profile"] = profile
}
b, _ := json.Marshal(body)
for attempt := 1; ; attempt++ {
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+engine+"/session", bytes.NewReader(b))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
data, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode == 200 {
var s struct{ WsUrl string }
json.Unmarshal(data, &s)
return s.WsUrl, nil
}
if (res.StatusCode != 429 && res.StatusCode != 503) || attempt == 5 {
return "", fmt.Errorf("launch %d %s", res.StatusCode, data)
}
wait, _ := strconv.Atoi(res.Header.Get("retry-after"))
if wait == 0 {
wait = 5
}
time.Sleep(time.Duration(wait) * time.Second)
}
}
// One session, one load: the status of the page's own response, then its state 8 s later.
func visit(pw *playwright.Playwright, s setup, url, proxy, profile string) cell {
ws, err := launch(s.Engine, s.Headless, proxy, profile)
if err != nil {
return cell{"error", 0}
}
bt := map[string]playwright.BrowserType{"chromium": pw.Chromium, "firefox": pw.Firefox, "webkit": pw.WebKit}[s.Type]
browser, err := bt.Connect(ws, playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
return cell{"error", 0}
}
defer browser.Close()
var ctx playwright.BrowserContext
if profile != "" {
ctx = browser.Contexts()[0]
} else if ctx, err = browser.NewContext(); err != nil {
return cell{"error", 0}
}
tab, err := ctx.NewPage()
if err != nil {
return cell{"error", 0}
}
res, err := tab.Goto(url, playwright.PageGotoOptions{WaitUntil: playwright.WaitUntilStateDomcontentloaded, Timeout: playwright.Float(45000)})
if err != nil {
return cell{"error", 0}
}
status := 0
if res != nil {
status = res.Status()
}
tab.WaitForTimeout(8000)
v, err := tab.Evaluate(state)
outcome, _ := v.(string)
if err != nil || outcome == "" {
outcome = "error"
}
if outcome == "content" && status >= 400 {
outcome = "blocked"
}
return cell{outcome, status}
}
// 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.
func site(label, url string) map[string]any {
pw, err := playwright.Run()
if err != nil {
log.Fatal(err)
}
defer pw.Stop()
proxy := "cdpfleet-resi-country-us-session-c70" + runID + label + "-lifetime-30"
results := map[string]cell{}
for _, s := range setups {
r := visit(pw, s, url, proxy, "")
if r.Outcome == "error" {
r = visit(pw, s, url, proxy, "")
}
results[s.ID] = r
}
// A returning visitor: Camoufox with a profile, a first visit and then a second session.
name := "case70-" + runID + "-" + label
camoufox := setups[len(setups)-1]
visit(pw, camoufox, url, proxy, name)
for i := 0; i < 60; i++ {
free := false
if list, ok := api("GET", "/v1/me/profiles")["profiles"].([]any); ok {
for _, p := range list {
if m := p.(map[string]any); m["name"] == name && m["in_use"] == false {
free = true
}
}
}
if free {
break
}
time.Sleep(2 * time.Second)
}
results["camoufox_returning"] = visit(pw, camoufox, url, proxy, name)
api("DELETE", "/v1/me/profiles/"+name)
return map[string]any{"site": label, "results": results}
}
func main() {
urls := strings.Split(os.Getenv("LAB_SITES"), ",")
sites := make([]map[string]any, len(urls))
var wg sync.WaitGroup
for i, u := range urls {
wg.Add(1)
go func(i int, u string) { defer wg.Done(); sites[i] = site(string(rune('A'+i)), u) }(i, u)
}
wg.Wait()
names := map[string]string{"camoufox_returning": "Camoufox, returning profile (2nd visit)"}
for _, s := range setups {
names[s.ID] = s.Name
}
out, _ := json.MarshalIndent(map[string]any{"measured_at": time.Now().UTC().Format("2006-01-02"), "setups": names, "sites": sites}, "", " ")
fmt.Println(string(out))
}
What we got
| Setup | Site A, a fashion retailer | Site B, a design-software site | Site C, a shipping company | Measured |
|---|---|---|---|---|
| Chrome, headful | content (200) | content (200) | content (200) | 2026-10-26 |
| Chrome, headless | content (200) | challenge (403) | challenge (403) | 2026-10-26 |
| Edge, headful | content (200) | content (200) | content (200) | 2026-10-26 |
| Brave, headful | content (200) | content (200) | content (200) | 2026-10-26 |
| Opera, headful | content (200) | content (200) | content (200) | 2026-10-26 |
| Yandex, headful | content (200) | content (200) | content (200) | 2026-10-26 |
| Whale, headful | content (200) | content (200) | content (200) | 2026-10-26 |
| Chromium, headless | content (200) | challenge (403) | challenge (403) | 2026-10-26 |
| Patchright, headful | content (200) | content (200) | challenge (403) | 2026-10-26 |
| Firefox, headful | content (200) | content (200) | challenge (403) | 2026-10-26 |
| WebKit, headful | content (200) | challenge (403) | challenge (403) | 2026-10-26 |
| Camoufox, headful | content (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
- Headless was the tell, not the engine. On 2026-10-26 headless Chrome and headless Chromium met Cloudflare's challenge (
403) on sites B and C in all five runs, while headful Chrome, Edge, Brave, Opera, Yandex and Whale, the same Chromium underneath, got the real page on all three sites in every run (one network error aside). - Camoufox got all three sites, every time, fresh or returning. Camoufox headful loaded A, B and C as
contentin all five runs, and so did the returning visit with a saved profile. Site C had stopped Camoufox in our sweep the day before: a real site's verdict moves from day to day, which is why these are dated snapshots. - Stealth patches didn't settle it. Patchright, headful, got site B in 3 of 5 runs and site C in 2 of 5; the plain branded browsers above did better on these sites.
- WebKit was challenged on B and C, Firefox on C. Both got site A; WebKit met the challenge on B and C in every run, and stock Firefox on C (it got B). Camoufox, built from Firefox source, got all three.
- Site A let everyone in. Every setup, headless ones included, got its page in every run: many sites run Cloudflare without challenging anyone by default.
- What to try first on a Cloudflare site: a headful branded browser or Camoufox. For the vendor's own test pages see case 64, for which vendor a site uses case 62, and for the other vendors cases 65 and 67; the overview is /bot-protection.