Cases / #64 · 2026-10-21 · Medium
Cloudflare: what gets through? Four browsers, three networks, four public test pages
Two Cloudflare challenge test pages and a Turnstile test page in two widget modes, loaded once per cell by headless Chromium, headful Chrome, headful Edge and Camoufox through your own proxy and a sticky cdpfleet-resi exit (Tier 1 and Tier 4). Outcome per cell, seconds to clearance, whether `cf_clearance` or a Turnstile token was issued, and what a returning visitor with a profile sees.
Chromium Google Chrome Microsoft Edge Camoufox
Run on production on 2026-10-21: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Cloudflare sits in front of a large share of the web, and "it doesn't load" can mean four different things there: the page loads, an automatic interstitial ("Just a moment…") passes by itself after a few seconds, it stays on an interactive check, or a block page answers. Which one you get depends on the browser (its TLS and HTTP/2 fingerprint, its JavaScript surface), the network the request comes from, and whether you are a returning visitor. This case measures those outcomes on public test pages made for exactly this (nowsecure.nl, scrapingcourse.com's Cloudflare challenge and peet.ws's Turnstile test), with one load per cell and nothing clicked or solved. It is a dated measurement, not an evasion guide: site owners choose their own Cloudflare settings, and Cloudflare changes its checks all the time. For the signals a page can read, see What a page can read about your browser.
What we used, and why
| What | Why |
|---|---|
| Four setups | Headless Chromium, headful Google Chrome, headful Microsoft Edge (all Chromium) and Camoufox (based on Firefox source; not affiliated with Mozilla), launched with record: false. |
proxy: your own vs cdpfleet-resi-country-us-session-<id> | Your own proxy (PROXY_URL, rotating) against a sticky residential exit. Every setup uses the same session name, so they share one exit IP and a difference between rows is the browser, not the network. |
cdpfleet-resi-tier-4-… | The cheaper Tier 4 pool on the two pages where the network could matter, with the two setups that behaved differently on Tier 1. |
| A fresh context per page | Every load is a first visit: no cookie from one page or setup reaches the next. |
page.evaluate() once a second | The title and opening text classify the page (content, Cloudflare's interstitial = challenge, "Sorry, you have been blocked" = blocked); on the Turnstile pages the hidden cf-turnstile-response field shows whether a token was issued. Nothing is clicked. |
context.cookies() | cf_clearance is the cookie Cloudflare sets when a check passes; only its presence is read. |
profile | A returning visitor: the same profile and the same sticky exit in a second session, to see whether the kept cf_clearance skips the check. |
How it works
- For each network (your own proxy, then the sticky Tier 1 exit) and each of the four setups, launch one session and load the four test pages once each, in a fresh context.
- After DOM ready, check the page once a second: a Cloudflare page gets 20 s to clear by itself (seconds to clearance recorded), a Turnstile widget 12 s to issue a token. Then wait up to 6 s for
cf_clearance. - Repeat three of the pages on a sticky Tier 4 exit with headful Chrome and Camoufox.
- Load scrapingcourse.com's challenge with Camoufox and a new
profile, end the session, wait for the profile to be saved, and load it again with the same profile and exit. 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)PROXY_URL— your proxy, e.g.http://user:[email protected]:8000
// 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 } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
const RUN = randomBytes(3).toString('hex');
// Sticky residential exits: every setup in a comparison uses the same session name, so the same exit IP.
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-c64${RUN}-lifetime-30` },
tier4: { label: 'cdpfleet-resi Tier 4, sticky US', proxy: `cdpfleet-resi-tier-4-country-us-session-c64${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 },
camoufox: { setup: 'Camoufox, defaults', engine: 'camoufox', type: firefox, headless: true },
};
// Public test pages only: two Cloudflare challenge pages and a Turnstile test page in two widget modes. `cf` pages may
// answer with Cloudflare's interstitial; `widget` pages embed Turnstile and get a token if it passes.
const PAGES = {
nowsecure: { url: 'https://nowsecure.nl/', kind: 'cf' },
scrapingcourse: { url: 'https://www.scrapingcourse.com/cloudflare-challenge', kind: 'cf' },
ts_noninteractive: { url: 'https://peet.ws/turnstile-test/non-interactive.html', kind: 'widget' },
ts_managed: { url: 'https://peet.ws/turnstile-test/managed.html', kind: 'widget' },
};
// What the page shows right now, in the lab's words. The same expression runs in every language.
const STATE = `(() => {
const t = (document.title + ' ' + (document.body ? document.body.innerText : '').slice(0, 400)).replace(/\\s+/g, ' ');
const el = document.querySelector('[name="cf-turnstile-response"]');
const state = /sorry, you have been blocked|attention required|error 1020|access denied/i.test(t) ? 'blocked'
: /just a moment|performing security verification|verify you are human|checking your browser/i.test(t) ? 'challenge' : 'page';
return { state, token: el && el.value ? el.value.length : 0 };
})()`;
// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async function launch(s, network, profile) {
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({ proxy: NETWORKS[network].proxy, headless: s.headless, record: false, ...(profile ? { profile } : {}) }),
});
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));
const hasClearance = async (context) => (await context.cookies()).some((c) => c.name === 'cf_clearance');
// One load, then watch the page once a second without touching it: an interstitial gets 20 s to
// clear by itself, a Turnstile widget 12 s to issue a token. A network failure is retried once.
async function visit(context, key) {
const page = PAGES[key];
const tab = await context.newPage();
const t0 = Date.now();
const cell = { outcome: 'challenge', seconds: null };
try {
try {
await tab.goto(page.url, { waitUntil: 'domcontentloaded', timeout: 45000 });
} catch {
await tab.goto(page.url, { waitUntil: 'domcontentloaded', timeout: 45000 });
}
} catch {
await tab.close();
return { outcome: 'error', seconds: null };
}
let sawChallenge = false;
const deadline = t0 + (page.kind === 'widget' ? 12000 : 20000);
while (Date.now() < deadline) {
const s = await tab.evaluate(STATE).catch(() => null); // null while the interstitial reloads the page
if (s?.state === 'blocked') { cell.outcome = 'blocked'; break; }
if (s?.state === 'challenge') sawChallenge = true;
if (s && page.kind === 'widget' && s.token > 0) { cell.outcome = 'cleared'; cell.seconds = Math.round((Date.now() - t0) / 1000); break; }
if (s?.state === 'page' && page.kind === 'cf') {
cell.outcome = sawChallenge ? 'cleared' : 'content';
if (sawChallenge) cell.seconds = Math.round((Date.now() - t0) / 1000);
break;
}
await sleep(1000);
}
if (page.kind === 'cf') {
// The clearance cookie can arrive a moment after the content: give it up to 6 s.
for (let i = 0; i < 6 && cell.outcome !== 'blocked' && !(await hasClearance(context)); i++) await sleep(1000);
cell.cf_clearance = await hasClearance(context);
} else {
cell.token = cell.outcome === 'cleared';
}
await tab.close();
return cell;
}
// One session per setup and network; a fresh context per page so every load is a first visit.
async function row(setupKey, network, pages) {
const s = SETUPS[setupKey];
const browser = await launch(s, network);
const out = { setup: s.setup, network: NETWORKS[network].label };
try {
for (const key of pages) {
const context = await browser.newContext();
out[key] = await visit(context, key);
await context.close();
}
} finally {
await browser.close();
}
return out;
}
async function api(method, path) {
const res = await fetch(`https://cdpfleet.com${path}`, { method, headers: { 'x-api-key': KEY } });
return res.json().catch(() => ({}));
}
const profileInfo = async (name) => (await api('GET', '/v1/me/profiles')).profiles?.find((p) => p.name === name);
// A returning visitor: the same profile and the same sticky exit, two sessions a few seconds apart.
async function returning(setupKey, network, key) {
const name = `case64-${RUN}`;
const visits = [];
for (let i = 0; i < 2; i++) {
const browser = await launch(SETUPS[setupKey], network, name);
try {
visits.push(await visit(browser.contexts()[0], key)); // the profile's own context; a newContext() is not saved
} finally {
await browser.close();
}
// The node saves the profile after the browser exits: wait until it is free again.
for (let j = 0; j < 60 && (await profileInfo(name))?.in_use !== false; j++) await sleep(2000);
}
await api('DELETE', `/v1/me/profiles/${name}`);
return { setup: SETUPS[setupKey].setup, network: NETWORKS[network].label, page: key, first: visits[0], second: visits[1] };
}
const matrix = [];
for (const network of ['own', 'tier1']) {
for (const setupKey of ['chromium', 'chrome', 'edge', 'camoufox']) matrix.push(await row(setupKey, network, Object.keys(PAGES)));
}
const tier4 = [];
for (const setupKey of ['chrome', 'camoufox']) tier4.push(await row(setupKey, 'tier4', ['nowsecure', 'scrapingcourse', 'ts_managed']));
const profile = await returning('camoufox', 'tier1', 'scrapingcourse');
console.log(JSON.stringify({ measured_at: new Date().toISOString().slice(0, 10), matrix, tier4, profile }, null, 2));
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL (one page load per page per setup and network; nothing is clicked or solved)
import json
import os
import secrets
import time
from datetime import datetime, timezone
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
RUN = secrets.token_hex(3)
# Sticky residential exits: every setup in a comparison uses the same session name, so the same exit IP.
NETWORKS = {
"own": {"label": "own proxy (rotating)", "proxy": os.environ["PROXY_URL"]},
"tier1": {"label": "cdpfleet-resi Tier 1, sticky US", "proxy": f"cdpfleet-resi-country-us-session-c64{RUN}-lifetime-30"},
"tier4": {"label": "cdpfleet-resi Tier 4, sticky US", "proxy": f"cdpfleet-resi-tier-4-country-us-session-c64{RUN}-lifetime-30"},
}
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},
"camoufox": {"setup": "Camoufox, defaults", "engine": "camoufox", "type": "firefox", "headless": True},
}
# Public test pages only: two Cloudflare challenge pages and a Turnstile test page in two widget modes.
# `cf` pages may answer with Cloudflare's interstitial; `widget` pages embed Turnstile and get a token if it passes.
PAGES = {
"nowsecure": {"url": "https://nowsecure.nl/", "kind": "cf"},
"scrapingcourse": {"url": "https://www.scrapingcourse.com/cloudflare-challenge", "kind": "cf"},
"ts_noninteractive": {"url": "https://peet.ws/turnstile-test/non-interactive.html", "kind": "widget"},
"ts_managed": {"url": "https://peet.ws/turnstile-test/managed.html", "kind": "widget"},
}
# What the page shows right now, in the lab's words. The same expression runs in every language.
STATE = r"""(() => {
const t = (document.title + ' ' + (document.body ? document.body.innerText : '').slice(0, 400)).replace(/\s+/g, ' ');
const el = document.querySelector('[name="cf-turnstile-response"]');
const state = /sorry, you have been blocked|attention required|error 1020|access denied/i.test(t) ? 'blocked'
: /just a moment|performing security verification|verify you are human|checking your browser/i.test(t) ? 'challenge' : 'page';
return { state, token: el && el.value ? el.value.length : 0 };
})()"""
# 503 means the fleet is momentarily busy: wait Retry-After and try again.
def launch(p, s, network, profile=None):
body = {"proxy": NETWORKS[network]["proxy"], "headless": s["headless"], "record": False}
if profile:
body["profile"] = profile
for attempt in range(1, 6):
res = requests.post(f"https://starter.cdpfleet.com/{s['engine']}/session", headers={"x-api-key": KEY}, json=body, timeout=120)
if res.ok:
return getattr(p, s["type"]).connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
if res.status_code not in (429, 503) or attempt == 5:
raise RuntimeError(f"launch {res.status_code} {res.text}")
time.sleep(int(res.headers.get("retry-after") or 5))
def has_clearance(context):
return any(c["name"] == "cf_clearance" for c in context.cookies())
# One load, then watch the page once a second without touching it: an interstitial gets 20 s to
# clear by itself, a Turnstile widget 12 s to issue a token. A network failure is retried once.
def visit(context, key):
page = PAGES[key]
tab = context.new_page()
t0 = time.time()
cell = {"outcome": "challenge", "seconds": None}
try:
try:
tab.goto(page["url"], wait_until="domcontentloaded", timeout=45000)
except Exception:
tab.goto(page["url"], wait_until="domcontentloaded", timeout=45000)
except Exception:
tab.close()
return {"outcome": "error", "seconds": None}
saw_challenge = False
deadline = t0 + (12 if page["kind"] == "widget" else 20)
while time.time() < deadline:
try:
s = tab.evaluate(STATE)
except Exception:
s = None # while the interstitial reloads the page
state = s["state"] if s else None
if state == "blocked":
cell["outcome"] = "blocked"
break
if state == "challenge":
saw_challenge = True
if s and page["kind"] == "widget" and s["token"] > 0:
cell["outcome"], cell["seconds"] = "cleared", round(time.time() - t0)
break
if state == "page" and page["kind"] == "cf":
cell["outcome"] = "cleared" if saw_challenge else "content"
if saw_challenge:
cell["seconds"] = round(time.time() - t0)
break
time.sleep(1)
if page["kind"] == "cf":
# The clearance cookie can arrive a moment after the content: give it up to 6 s.
for _ in range(6):
if cell["outcome"] == "blocked" or has_clearance(context):
break
time.sleep(1)
cell["cf_clearance"] = has_clearance(context)
else:
cell["token"] = cell["outcome"] == "cleared"
tab.close()
return cell
# One session per setup and network; a fresh context per page so every load is a first visit.
def row(p, setup_key, network, pages):
s = SETUPS[setup_key]
browser = launch(p, s, network)
out = {"setup": s["setup"], "network": NETWORKS[network]["label"]}
try:
for key in pages:
context = browser.new_context()
out[key] = visit(context, key)
context.close()
finally:
browser.close()
return out
def api(method, path):
try:
return requests.request(method, f"https://cdpfleet.com{path}", headers={"x-api-key": KEY}, timeout=60).json()
except ValueError:
return {}
def profile_info(name):
return next((x for x in api("GET", "/v1/me/profiles").get("profiles", []) if x["name"] == name), None)
# A returning visitor: the same profile and the same sticky exit, two sessions a few seconds apart.
def returning(p, setup_key, network, key):
name = f"case64-{RUN}"
visits = []
for _ in range(2):
browser = launch(p, SETUPS[setup_key], network, name)
try:
visits.append(visit(browser.contexts[0], key)) # the profile's own context; a new_context() is not saved
finally:
browser.close()
# The node saves the profile after the browser exits: wait until it is free again.
for _ in range(60):
info = profile_info(name)
if info and info["in_use"] is False:
break
time.sleep(2)
api("DELETE", f"/v1/me/profiles/{name}")
return {"setup": SETUPS[setup_key]["setup"], "network": NETWORKS[network]["label"], "page": key, "first": visits[0], "second": visits[1]}
with sync_playwright() as p:
matrix = [row(p, setup_key, network, list(PAGES)) for network in ("own", "tier1") for setup_key in ("chromium", "chrome", "edge", "camoufox")]
tier4 = [row(p, setup_key, "tier4", ["nowsecure", "scrapingcourse", "ts_managed"]) for setup_key in ("chrome", "camoufox")]
profile = returning(p, "camoufox", "tier1", "scrapingcourse")
print(json.dumps({"measured_at": datetime.now(timezone.utc).strftime("%Y-%m-%d"), "matrix": matrix, "tier4": tier4, "profile": profile}, indent=2))
// 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, PROXY_URL (one page load per page per setup and network; nothing is clicked or solved)
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.*;
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 Network(String label, String proxy) {}
record Setup(String name, String engine, String type, boolean headless) {}
record TestPage(String url, String kind) {}
// Sticky residential exits: every setup in a comparison uses the same session name, so the same exit IP.
static final Map<String, Network> NETWORKS = Map.of(
"own", new Network("own proxy (rotating)", System.getenv("PROXY_URL")),
"tier1", new Network("cdpfleet-resi Tier 1, sticky US", "cdpfleet-resi-country-us-session-c64" + RUN + "-lifetime-30"),
"tier4", new Network("cdpfleet-resi Tier 4, sticky US", "cdpfleet-resi-tier-4-country-us-session-c64" + RUN + "-lifetime-30"));
static final Map<String, Setup> SETUPS = Map.of(
"chromium", new Setup("Chromium, headless", "chromium", "chromium", true),
"chrome", new Setup("Google Chrome, headful", "chrome", "chromium", false),
"edge", new Setup("Microsoft Edge, headful", "edge", "chromium", false),
"camoufox", new Setup("Camoufox, defaults", "camoufox", "firefox", true));
// Public test pages only: two Cloudflare challenge pages and a Turnstile test page in two widget modes.
// "cf" pages may answer with Cloudflare's interstitial; "widget" pages embed Turnstile and get a token if it passes.
static final Map<String, TestPage> PAGES = new LinkedHashMap<>();
static {
PAGES.put("nowsecure", new TestPage("https://nowsecure.nl/", "cf"));
PAGES.put("scrapingcourse", new TestPage("https://www.scrapingcourse.com/cloudflare-challenge", "cf"));
PAGES.put("ts_noninteractive", new TestPage("https://peet.ws/turnstile-test/non-interactive.html", "widget"));
PAGES.put("ts_managed", new TestPage("https://peet.ws/turnstile-test/managed.html", "widget"));
}
// What the page shows right now, in the lab's words. The same expression runs in every language.
static final String STATE = """
(() => {
const t = (document.title + ' ' + (document.body ? document.body.innerText : '').slice(0, 400)).replace(/\\s+/g, ' ');
const el = document.querySelector('[name="cf-turnstile-response"]');
const state = /sorry, you have been blocked|attention required|error 1020|access denied/i.test(t) ? 'blocked'
: /just a moment|performing security verification|verify you are human|checking your browser/i.test(t) ? 'challenge' : 'page';
return { state, token: el && el.value ? el.value.length : 0 };
})()""";
static Playwright playwright;
// 503 means the fleet is momentarily busy: wait Retry-After and try again.
static Browser launch(Setup s, String network, String profile) throws Exception {
JsonObject body = new JsonObject();
body.addProperty("proxy", NETWORKS.get(network).proxy());
body.addProperty("headless", s.headless());
body.addProperty("record", false);
if (profile != null) body.addProperty("profile", profile);
for (int attempt = 1; ; attempt++) {
HttpResponse<String> res = HTTP.send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/" + s.engine() + "/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body.toString())).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() == 200) {
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
BrowserType type = s.type().equals("firefox") ? playwright.firefox() : playwright.chromium();
return type.connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
}
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 boolean hasClearance(BrowserContext context) {
return context.cookies().stream().anyMatch(c -> c.name.equals("cf_clearance"));
}
// One load, then watch the page once a second without touching it: an interstitial gets 20 s to
// clear by itself, a Turnstile widget 12 s to issue a token. A network failure is retried once.
static JsonObject visit(BrowserContext context, String key) throws Exception {
TestPage page = PAGES.get(key);
Page tab = context.newPage();
long t0 = System.currentTimeMillis();
JsonObject cell = new JsonObject();
cell.addProperty("outcome", "challenge");
cell.add("seconds", JsonNull.INSTANCE);
Page.NavigateOptions nav = new Page.NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED).setTimeout(45000);
try {
try {
tab.navigate(page.url(), nav);
} catch (PlaywrightException first) {
tab.navigate(page.url(), nav);
}
} catch (PlaywrightException err) {
tab.close();
cell.addProperty("outcome", "error");
return cell;
}
boolean sawChallenge = false;
long deadline = t0 + (page.kind().equals("widget") ? 12000 : 20000);
while (System.currentTimeMillis() < deadline) {
Map<?, ?> s = null;
try {
s = (Map<?, ?>) tab.evaluate(STATE);
} catch (PlaywrightException err) {
// while the interstitial reloads the page
}
String state = s == null ? null : (String) s.get("state");
long seconds = Math.round((System.currentTimeMillis() - t0) / 1000.0);
if ("blocked".equals(state)) { cell.addProperty("outcome", "blocked"); break; }
if ("challenge".equals(state)) sawChallenge = true;
if (s != null && page.kind().equals("widget") && ((Number) s.get("token")).intValue() > 0) {
cell.addProperty("outcome", "cleared");
cell.addProperty("seconds", seconds);
break;
}
if ("page".equals(state) && page.kind().equals("cf")) {
cell.addProperty("outcome", sawChallenge ? "cleared" : "content");
if (sawChallenge) cell.addProperty("seconds", seconds);
break;
}
Thread.sleep(1000);
}
if (page.kind().equals("cf")) {
// The clearance cookie can arrive a moment after the content: give it up to 6 s.
for (int i = 0; i < 6 && !cell.get("outcome").getAsString().equals("blocked") && !hasClearance(context); i++) Thread.sleep(1000);
cell.addProperty("cf_clearance", hasClearance(context));
} else {
cell.addProperty("token", cell.get("outcome").getAsString().equals("cleared"));
}
tab.close();
return cell;
}
// One session per setup and network; a fresh context per page so every load is a first visit.
static JsonObject row(String setupKey, String network, List<String> pages) throws Exception {
Setup s = SETUPS.get(setupKey);
Browser browser = launch(s, network, null);
JsonObject out = new JsonObject();
out.addProperty("setup", s.name());
out.addProperty("network", NETWORKS.get(network).label());
try {
for (String key : pages) {
BrowserContext context = browser.newContext();
out.add(key, visit(context, key));
context.close();
}
} finally {
browser.close();
}
return out;
}
static JsonObject api(String method, String path) throws Exception {
HttpResponse<String> res = HTTP.send(HttpRequest.newBuilder(URI.create("https://cdpfleet.com" + path))
.header("x-api-key", KEY).method(method, HttpRequest.BodyPublishers.noBody()).build(), HttpResponse.BodyHandlers.ofString());
try {
return JsonParser.parseString(res.body()).getAsJsonObject();
} catch (RuntimeException err) {
return new JsonObject();
}
}
static JsonObject profileInfo(String name) throws Exception {
JsonObject body = api("GET", "/v1/me/profiles");
if (!body.has("profiles")) return null;
for (JsonElement p : body.getAsJsonArray("profiles")) if (p.getAsJsonObject().get("name").getAsString().equals(name)) return p.getAsJsonObject();
return null;
}
// A returning visitor: the same profile and the same sticky exit, two sessions a few seconds apart.
static JsonObject returning(String setupKey, String network, String key) throws Exception {
String name = "case64-" + RUN;
List<JsonObject> visits = new ArrayList<>();
for (int i = 0; i < 2; i++) {
Browser browser = launch(SETUPS.get(setupKey), network, name);
try {
visits.add(visit(browser.contexts().get(0), key)); // the profile's own context; a newContext() is not saved
} finally {
browser.close();
}
// The node saves the profile after the browser exits: wait until it is free again.
for (int j = 0; j < 60; j++) {
JsonObject info = profileInfo(name);
if (info != null && !info.get("in_use").getAsBoolean()) break;
Thread.sleep(2000);
}
}
api("DELETE", "/v1/me/profiles/" + name);
JsonObject out = new JsonObject();
out.addProperty("setup", SETUPS.get(setupKey).name());
out.addProperty("network", NETWORKS.get(network).label());
out.addProperty("page", key);
out.add("first", visits.get(0));
out.add("second", visits.get(1));
return out;
}
public static void main(String[] args) throws Exception {
JsonObject result = new JsonObject();
try (Playwright pw = Playwright.create()) {
playwright = pw;
JsonArray matrix = new JsonArray();
for (String network : List.of("own", "tier1"))
for (String setupKey : List.of("chromium", "chrome", "edge", "camoufox")) matrix.add(row(setupKey, network, new ArrayList<>(PAGES.keySet())));
JsonArray tier4 = new JsonArray();
for (String setupKey : List.of("chrome", "camoufox")) tier4.add(row(setupKey, "tier4", List.of("nowsecure", "scrapingcourse", "ts_managed")));
JsonObject profile = returning("camoufox", "tier1", "scrapingcourse");
result.addProperty("measured_at", Instant.now().toString().substring(0, 10));
result.add("matrix", matrix);
result.add("tier4", tier4);
result.add("profile", profile);
}
System.out.println(GSON.toJson(result));
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL (one page load per page per setup and network; nothing is clicked or solved)
using System.Diagnostics;
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();
// Sticky residential exits: every setup in a comparison uses the same session name, so the same exit IP.
var networks = new Dictionary<string, (string Label, string? Proxy)>
{
["own"] = ("own proxy (rotating)", Environment.GetEnvironmentVariable("PROXY_URL")),
["tier1"] = ("cdpfleet-resi Tier 1, sticky US", $"cdpfleet-resi-country-us-session-c64{run}-lifetime-30"),
["tier4"] = ("cdpfleet-resi Tier 4, sticky US", $"cdpfleet-resi-tier-4-country-us-session-c64{run}-lifetime-30"),
};
var setups = new Dictionary<string, (string Name, string Engine, IBrowserType Type, bool Headless)>
{
["chromium"] = ("Chromium, headless", "chromium", playwright.Chromium, true),
["chrome"] = ("Google Chrome, headful", "chrome", playwright.Chromium, false),
["edge"] = ("Microsoft Edge, headful", "edge", playwright.Chromium, false),
["camoufox"] = ("Camoufox, defaults", "camoufox", playwright.Firefox, true),
};
// Public test pages only: two Cloudflare challenge pages and a Turnstile test page in two widget modes.
// "cf" pages may answer with Cloudflare's interstitial; "widget" pages embed Turnstile and get a token if it passes.
var pages = new (string Key, string Url, string Kind)[]
{
("nowsecure", "https://nowsecure.nl/", "cf"),
("scrapingcourse", "https://www.scrapingcourse.com/cloudflare-challenge", "cf"),
("ts_noninteractive", "https://peet.ws/turnstile-test/non-interactive.html", "widget"),
("ts_managed", "https://peet.ws/turnstile-test/managed.html", "widget"),
};
// What the page shows right now, in the lab's words. The same expression runs in every language.
const string State = """
(() => {
const t = (document.title + ' ' + (document.body ? document.body.innerText : '').slice(0, 400)).replace(/\s+/g, ' ');
const el = document.querySelector('[name="cf-turnstile-response"]');
const state = /sorry, you have been blocked|attention required|error 1020|access denied/i.test(t) ? 'blocked'
: /just a moment|performing security verification|verify you are human|checking your browser/i.test(t) ? 'challenge' : 'page';
return { state, token: el && el.value ? el.value.length : 0 };
})()
""";
// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async Task<IBrowser> Launch(string setupKey, string network, string? profile = null)
{
var s = setups[setupKey];
var body = new JsonObject { ["proxy"] = networks[network].Proxy, ["headless"] = s.Headless, ["record"] = false };
if (profile != null) body["profile"] = profile;
for (var attempt = 1; ; attempt++)
{
var res = await http.PostAsJsonAsync($"https://starter.cdpfleet.com/{s.Engine}/session", body);
if (res.IsSuccessStatusCode)
{
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
return await s.Type.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
}
var code = (int)res.StatusCode;
if ((code != 429 && code != 503) || attempt == 5) throw new Exception($"launch {code} {await res.Content.ReadAsStringAsync()}");
await Task.Delay(TimeSpan.FromSeconds(res.Headers.RetryAfter?.Delta?.TotalSeconds ?? 5));
}
}
async Task<bool> HasClearance(IBrowserContext context) => (await context.CookiesAsync()).Any(c => c.Name == "cf_clearance");
// One load, then watch the page once a second without touching it: an interstitial gets 20 s to
// clear by itself, a Turnstile widget 12 s to issue a token. A network failure is retried once.
async Task<JsonObject> Visit(IBrowserContext context, string pageKey)
{
var page = pages.First(p => p.Key == pageKey);
var tab = await context.NewPageAsync();
var clock = Stopwatch.StartNew();
var cell = new JsonObject { ["outcome"] = "challenge", ["seconds"] = null };
var nav = new PageGotoOptions { WaitUntil = WaitUntilState.DOMContentLoaded, Timeout = 45000 };
try
{
try { await tab.GotoAsync(page.Url, nav); }
catch (PlaywrightException) { await tab.GotoAsync(page.Url, nav); }
}
catch (Exception)
{
await tab.CloseAsync();
return new JsonObject { ["outcome"] = "error", ["seconds"] = null };
}
var sawChallenge = false;
var limit = page.Kind == "widget" ? 12000 : 20000;
while (clock.ElapsedMilliseconds < limit)
{
string? state = null;
var token = 0;
try
{
var s = await tab.EvaluateAsync<JsonElement>(State);
state = s.GetProperty("state").GetString();
token = s.GetProperty("token").GetInt32();
}
catch (PlaywrightException) { } // while the interstitial reloads the page
var seconds = (int)Math.Round(clock.ElapsedMilliseconds / 1000.0);
if (state == "blocked") { cell["outcome"] = "blocked"; break; }
if (state == "challenge") sawChallenge = true;
if (state != null && page.Kind == "widget" && token > 0) { cell["outcome"] = "cleared"; cell["seconds"] = seconds; break; }
if (state == "page" && page.Kind == "cf")
{
cell["outcome"] = sawChallenge ? "cleared" : "content";
if (sawChallenge) cell["seconds"] = seconds;
break;
}
await Task.Delay(1000);
}
if (page.Kind == "cf")
{
// The clearance cookie can arrive a moment after the content: give it up to 6 s.
for (var i = 0; i < 6 && (string?)cell["outcome"] != "blocked" && !await HasClearance(context); i++) await Task.Delay(1000);
cell["cf_clearance"] = await HasClearance(context);
}
else
{
cell["token"] = (string?)cell["outcome"] == "cleared";
}
await tab.CloseAsync();
return cell;
}
// One session per setup and network; a fresh context per page so every load is a first visit.
async Task<JsonObject> Row(string setupKey, string network, string[] keys)
{
var browser = await Launch(setupKey, network);
var row = new JsonObject { ["setup"] = setups[setupKey].Name, ["network"] = networks[network].Label };
try
{
foreach (var k in keys)
{
var context = await browser.NewContextAsync();
row[k] = await Visit(context, k);
await context.CloseAsync();
}
}
finally
{
await browser.CloseAsync();
}
return row;
}
async Task<JsonElement?> ProfileInfo(string name)
{
var body = await http.GetFromJsonAsync<JsonElement>("https://cdpfleet.com/v1/me/profiles");
if (!body.TryGetProperty("profiles", out var list)) return null;
foreach (var p in list.EnumerateArray()) if (p.GetProperty("name").GetString() == name) return p;
return null;
}
// A returning visitor: the same profile and the same sticky exit, two sessions a few seconds apart.
async Task<JsonObject> Returning(string setupKey, string network, string pageKey)
{
var name = $"case64-{run}";
var visits = new List<JsonObject>();
for (var i = 0; i < 2; i++)
{
var browser = await Launch(setupKey, network, name);
try
{
visits.Add(await Visit(browser.Contexts[0], pageKey)); // the profile's own context; a NewContextAsync() is not saved
}
finally
{
await browser.CloseAsync();
}
// The node saves the profile after the browser exits: wait until it is free again.
for (var j = 0; j < 60; j++)
{
var info = await ProfileInfo(name);
if (info is { } p && !p.GetProperty("in_use").GetBoolean()) break;
await Task.Delay(2000);
}
}
await http.DeleteAsync($"https://cdpfleet.com/v1/me/profiles/{name}");
return new JsonObject
{
["setup"] = setups[setupKey].Name, ["network"] = networks[network].Label, ["page"] = pageKey,
["first"] = visits[0], ["second"] = visits[1],
};
}
var matrix = new JsonArray();
foreach (var network in new[] { "own", "tier1" })
foreach (var setupKey in new[] { "chromium", "chrome", "edge", "camoufox" })
matrix.Add(await Row(setupKey, network, pages.Select(p => p.Key).ToArray()));
var tier4 = new JsonArray();
foreach (var setupKey in new[] { "chrome", "camoufox" }) tier4.Add(await Row(setupKey, "tier4", new[] { "nowsecure", "scrapingcourse", "ts_managed" }));
var profile = await Returning("camoufox", "tier1", "scrapingcourse");
var result = new JsonObject { ["measured_at"] = DateTime.UtcNow.ToString("yyyy-MM-dd"), ["matrix"] = matrix, ["tier4"] = tier4, ["profile"] = profile };
Console.WriteLine(result.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, PROXY_URL (one page load per page per setup and network; nothing is clicked or solved)
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"strconv"
"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 network struct{ Label, Proxy string }
// Sticky residential exits: every setup in a comparison uses the same session name, so the same exit IP.
var networks = map[string]network{
"own": {"own proxy (rotating)", os.Getenv("PROXY_URL")},
"tier1": {"cdpfleet-resi Tier 1, sticky US", "cdpfleet-resi-country-us-session-c64" + runID + "-lifetime-30"},
"tier4": {"cdpfleet-resi Tier 4, sticky US", "cdpfleet-resi-tier-4-country-us-session-c64" + runID + "-lifetime-30"},
}
type setup struct {
Name, Engine, Type string
Headless bool
}
var setups = map[string]setup{
"chromium": {"Chromium, headless", "chromium", "chromium", true},
"chrome": {"Google Chrome, headful", "chrome", "chromium", false},
"edge": {"Microsoft Edge, headful", "edge", "chromium", false},
"camoufox": {"Camoufox, defaults", "camoufox", "firefox", true},
}
type testPage struct{ URL, Kind string }
// Public test pages only: two Cloudflare challenge pages and a Turnstile test page in two widget modes.
// "cf" pages may answer with Cloudflare's interstitial; "widget" pages embed Turnstile and get a token if it passes.
var pages = map[string]testPage{
"nowsecure": {"https://nowsecure.nl/", "cf"},
"scrapingcourse": {"https://www.scrapingcourse.com/cloudflare-challenge", "cf"},
"ts_noninteractive": {"https://peet.ws/turnstile-test/non-interactive.html", "widget"},
"ts_managed": {"https://peet.ws/turnstile-test/managed.html", "widget"},
}
var pageOrder = []string{"nowsecure", "scrapingcourse", "ts_noninteractive", "ts_managed"}
// What the page shows right now, in the lab's words. The same expression runs in every language.
const state = `(() => {
const t = (document.title + ' ' + (document.body ? document.body.innerText : '').slice(0, 400)).replace(/\s+/g, ' ');
const el = document.querySelector('[name="cf-turnstile-response"]');
const state = /sorry, you have been blocked|attention required|error 1020|access denied/i.test(t) ? 'blocked'
: /just a moment|performing security verification|verify you are human|checking your browser/i.test(t) ? 'challenge' : 'page';
return { state, token: el && el.value ? el.value.length : 0 };
})()`
type cell struct {
Outcome string `json:"outcome"`
Seconds *int `json:"seconds"`
CfClearance *bool `json:"cf_clearance,omitempty"`
Token *bool `json:"token,omitempty"`
}
type row struct {
Setup string `json:"setup"`
Network string `json:"network"`
Nowsecure *cell `json:"nowsecure,omitempty"`
Scrapingcourse *cell `json:"scrapingcourse,omitempty"`
TsNoninteractive *cell `json:"ts_noninteractive,omitempty"`
TsManaged *cell `json:"ts_managed,omitempty"`
}
func (r *row) set(k string, c *cell) {
switch k {
case "nowsecure":
r.Nowsecure = c
case "scrapingcourse":
r.Scrapingcourse = c
case "ts_noninteractive":
r.TsNoninteractive = c
case "ts_managed":
r.TsManaged = c
}
}
var pw *playwright.Playwright
// 503 means the fleet is momentarily busy: wait Retry-After and try again.
func launch(s setup, net, profile string) playwright.Browser {
body := map[string]any{"proxy": networks[net].Proxy, "headless": s.Headless, "record": false}
if profile != "" {
body["profile"] = profile
}
payload, _ := json.Marshal(body)
for attempt := 1; ; attempt++ {
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+s.Engine+"/session", bytes.NewReader(payload))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
msg, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode == 200 {
var session struct{ WsURL string }
json.Unmarshal(msg, &session)
bt := pw.Chromium
if s.Type == "firefox" {
bt = pw.Firefox
}
browser, err := bt.Connect(session.WsURL, playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
log.Fatal(err)
}
return browser
}
if (res.StatusCode != 429 && res.StatusCode != 503) || attempt == 5 {
log.Fatalf("launch %d %s", res.StatusCode, msg)
}
wait, _ := strconv.Atoi(res.Header.Get("retry-after"))
if wait == 0 {
wait = 5
}
time.Sleep(time.Duration(wait) * time.Second)
}
}
func hasClearance(ctx playwright.BrowserContext) bool {
cookies, _ := ctx.Cookies()
for _, c := range cookies {
if c.Name == "cf_clearance" {
return true
}
}
return false
}
// One load, then watch the page once a second without touching it: an interstitial gets 20 s to
// clear by itself, a Turnstile widget 12 s to issue a token. A network failure is retried once.
func visit(ctx playwright.BrowserContext, k string) *cell {
page := pages[k]
tab, err := ctx.NewPage()
if err != nil {
log.Fatal(err)
}
t0 := time.Now()
c := &cell{Outcome: "challenge"}
nav := playwright.PageGotoOptions{WaitUntil: playwright.WaitUntilStateDomcontentloaded, Timeout: playwright.Float(45000)}
if _, err := tab.Goto(page.URL, nav); err != nil {
if _, err := tab.Goto(page.URL, nav); err != nil {
tab.Close()
return &cell{Outcome: "error"}
}
}
sawChallenge := false
limit := 20 * time.Second
if page.Kind == "widget" {
limit = 12 * time.Second
}
for time.Since(t0) < limit {
st, token := "", 0
if v, err := tab.Evaluate(state); err == nil { // fails while the interstitial reloads the page
m := v.(map[string]any)
st, _ = m["state"].(string)
switch n := m["token"].(type) {
case int:
token = n
case float64:
token = int(n)
}
}
seconds := int(math.Round(time.Since(t0).Seconds()))
if st == "blocked" {
c.Outcome = "blocked"
break
}
if st == "challenge" {
sawChallenge = true
}
if st != "" && page.Kind == "widget" && token > 0 {
c.Outcome, c.Seconds = "cleared", &seconds
break
}
if st == "page" && page.Kind == "cf" {
c.Outcome = "content"
if sawChallenge {
c.Outcome, c.Seconds = "cleared", &seconds
}
break
}
time.Sleep(time.Second)
}
if page.Kind == "cf" {
// The clearance cookie can arrive a moment after the content: give it up to 6 s.
for i := 0; i < 6 && c.Outcome != "blocked" && !hasClearance(ctx); i++ {
time.Sleep(time.Second)
}
cf := hasClearance(ctx)
c.CfClearance = &cf
} else {
tok := c.Outcome == "cleared"
c.Token = &tok
}
tab.Close()
return c
}
// One session per setup and network; a fresh context per page so every load is a first visit.
func measure(setupKey, net string, keys []string) row {
s := setups[setupKey]
browser := launch(s, net, "")
defer browser.Close()
r := row{Setup: s.Name, Network: networks[net].Label}
for _, k := range keys {
ctx, err := browser.NewContext()
if err != nil {
log.Fatal(err)
}
r.set(k, visit(ctx, k))
ctx.Close()
}
return r
}
func api(method, path string) map[string]any {
req, _ := http.NewRequest(method, "https://cdpfleet.com"+path, nil)
req.Header.Set("x-api-key", key)
out := map[string]any{}
if res, err := http.DefaultClient.Do(req); err == nil {
json.NewDecoder(res.Body).Decode(&out)
res.Body.Close()
}
return out
}
func profileInfo(name string) map[string]any {
list, _ := api("GET", "/v1/me/profiles")["profiles"].([]any)
for _, p := range list {
if m := p.(map[string]any); m["name"] == name {
return m
}
}
return nil
}
type returnVisit struct {
Setup string `json:"setup"`
Network string `json:"network"`
Page string `json:"page"`
First *cell `json:"first"`
Second *cell `json:"second"`
}
// A returning visitor: the same profile and the same sticky exit, two sessions a few seconds apart.
func returning(setupKey, net, k string) returnVisit {
name := "case64-" + runID
visits := []*cell{}
for i := 0; i < 2; i++ {
browser := launch(setups[setupKey], net, name)
visits = append(visits, visit(browser.Contexts()[0], k)) // the profile's own context; a NewContext() is not saved
browser.Close()
// The node saves the profile after the browser exits: wait until it is free again.
for j := 0; j < 60; j++ {
if info := profileInfo(name); info != nil && info["in_use"] == false {
break
}
time.Sleep(2 * time.Second)
}
}
api("DELETE", "/v1/me/profiles/"+name)
return returnVisit{setups[setupKey].Name, networks[net].Label, k, visits[0], visits[1]}
}
func main() {
var err error
pw, err = playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
if err != nil {
log.Fatal(err)
}
defer pw.Stop()
matrix := []row{}
for _, net := range []string{"own", "tier1"} {
for _, setupKey := range []string{"chromium", "chrome", "edge", "camoufox"} {
matrix = append(matrix, measure(setupKey, net, pageOrder))
}
}
tier4 := []row{}
for _, setupKey := range []string{"chrome", "camoufox"} {
tier4 = append(tier4, measure(setupKey, "tier4", []string{"nowsecure", "scrapingcourse", "ts_managed"}))
}
profile := returning("camoufox", "tier1", "scrapingcourse")
out, _ := json.MarshalIndent(map[string]any{"measured_at": time.Now().UTC().Format("2006-01-02"), "matrix": matrix, "tier4": tier4, "profile": profile}, "", " ")
fmt.Println(string(out))
}
What we got
| Setup | Network | nowsecure.nl | scrapingcourse.com challenge | Turnstile, non-interactive | Turnstile, managed | Measured |
|---|---|---|---|---|---|---|
| Chromium, headless | own proxy (rotating) | content (cf_clearance) | error | cleared (6 s, token) | challenge | 2026-10-21 |
| Google Chrome, headful | own proxy (rotating) | content (cf_clearance) | challenge | challenge | challenge | 2026-10-21 |
| Microsoft Edge, headful | own proxy (rotating) | challenge (cf_clearance) | challenge | cleared (8 s, token) | challenge | 2026-10-21 |
| Camoufox, defaults | own proxy (rotating) | content (cf_clearance) | cleared (2 s) | cleared (8 s, token) | challenge | 2026-10-21 |
| Chromium, headless | cdpfleet-resi Tier 1, sticky US | content (cf_clearance) | challenge | cleared (7 s, token) | challenge | 2026-10-21 |
| Google Chrome, headful | cdpfleet-resi Tier 1, sticky US | content (cf_clearance) | challenge | cleared (7 s, token) | challenge | 2026-10-21 |
| Microsoft Edge, headful | cdpfleet-resi Tier 1, sticky US | content (cf_clearance) | challenge | cleared (7 s, token) | challenge | 2026-10-21 |
| Camoufox, defaults | cdpfleet-resi Tier 1, sticky US | content (cf_clearance) | cleared (13 s, cf_clearance) | cleared (8 s, token) | challenge | 2026-10-21 |
| Google Chrome, headful | cdpfleet-resi Tier 4, sticky US | content (cf_clearance) | challenge | — | challenge | 2026-10-21 |
| Camoufox, defaults | cdpfleet-resi Tier 4, sticky US | content (cf_clearance) | cleared (18 s, cf_clearance) | — | challenge | 2026-10-21 |
| Camoufox, defaults, returning (profile) | cdpfleet-resi Tier 1, sticky US | — | 1st visit: cleared (11 s, cf_clearance) → 2nd: content (cf_clearance) | — | — | 2026-10-21 |
From the Node.js run on 2026-10-21. 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-21",
"matrix": [
{
"setup": "Chromium, headless",
"network": "own proxy (rotating)",
"nowsecure": {
"outcome": "content",
"seconds": null,
"cf_clearance": true
},
"scrapingcourse": {
"outcome": "error",
"seconds": null
},
"ts_noninteractive": {
"outcome": "cleared",
"seconds": 6,
"token": true
},
"ts_managed": {
"outcome": "challenge",
"seconds": null,
"token": false
}
},
{
"setup": "Google Chrome, headful",
"network": "own proxy (rotating)",
"nowsecure": {
"outcome": "content",
"seconds": null,
"cf_clearance": true
},
"scrapingcourse": {
"outcome": "challenge",
"seconds": null,
"cf_clearance": false
},
"ts_noninteractive": {
"outcome": "challenge",
"seconds": null,
"token": false
},
"ts_managed": {
"outcome": "challenge",
"seconds": null,
"token": false
}
},
{
"setup": "Microsoft Edge, headful",
"network": "own proxy (rotating)",
"nowsecure": {
"outcome": "challenge",
"seconds": null,
"cf_clearance": true
},
"scrapingcourse": {
"outcome": "challenge",
"seconds": null,
"cf_clearance": false
},
"ts_noninteractive": {
"outcome": "cleared",
"seconds": 8,
"token": true
},
"ts_managed": {
"outcome": "challenge",
"seconds": null,
"token": false
}
},
{
"setup": "Camoufox, defaults",
"network": "own proxy (rotating)",
"nowsecure": {
"outcome": "content",
"seconds": null,
"cf_clearance": true
},
"scrapingcourse": {
"outcome": "cleared",
"seconds": 2,
"cf_clearance": false
},
"ts_noninteractive": {
"outcome": "cleared",
"seconds": 8,
"token": true
},
"ts_managed": {
"outcome": "challenge",
"seconds": null,
"token": false
}
},
{
"setup": "Chromium, headless",
"network": "cdpfleet-resi Tier 1, sticky US",
"nowsecure": {
"outcome": "content",
"seconds": null,
"cf_clearance": true
},
"scrapingcourse": {
"outcome": "challenge",
"seconds": null,
"cf_clearance": false
},
"ts_noninteractive": {
"outcome": "cleared",
"seconds": 7,
"token": true
},
"ts_managed": {
"outcome": "challenge",
"seconds": null,
"token": false
}
},
{
"setup": "Google Chrome, headful",
"network": "cdpfleet-resi Tier 1, sticky US",
"nowsecure": {
"outcome": "content",
"seconds": null,
"cf_clearance": true
},
"scrapingcourse": {
"outcome": "challenge",
"seconds": null,
"cf_clearance": false
},
"ts_noninteractive": {
"outcome": "cleared",
"seconds": 7,
"token": true
},
"ts_managed": {
"outcome": "challenge",
"seconds": null,
"token": false
}
},
{
"setup": "Microsoft Edge, headful",
"network": "cdpfleet-resi Tier 1, sticky US",
"nowsecure": {
"outcome": "content",
"seconds": null,
"cf_clearance": true
},
"scrapingcourse": {
"outcome": "challenge",
"seconds": null,
"cf_clearance": false
},
"ts_noninteractive": {
"outcome": "cleared",
"seconds": 7,
"token": true
},
"ts_managed": {
"outcome": "challenge",
"seconds": null,
"token": false
}
},
{
"setup": "Camoufox, defaults",
"network": "cdpfleet-resi Tier 1, sticky US",
"nowsecure": {
"outcome": "content",
"seconds": null,
"cf_clearance": true
},
"scrapingcourse": {
"outcome": "cleared",
"seconds": 13,
"cf_clearance": true
},
"ts_noninteractive": {
"outcome": "cleared",
"seconds": 8,
"token": true
},
"ts_managed": {
"outcome": "challenge",
"seconds": null,
"token": false
}
}
],
"tier4": [
{
"setup": "Google Chrome, headful",
"network": "cdpfleet-resi Tier 4, sticky US",
"nowsecure": {
"outcome": "content",
"seconds": null,
"cf_clearance": true
},
"scrapingcourse": {
"outcome": "challenge",
"seconds": null,
"cf_clearance": false
},
"ts_managed": {
"outcome": "challenge",
"seconds": null,
"token": false
}
},
{
"setup": "Camoufox, defaults",
"network": "cdpfleet-resi Tier 4, sticky US",
"nowsecure": {
"outcome": "content",
"seconds": null,
"cf_clearance": true
},
"scrapingcourse": {
"outcome": "cleared",
"seconds": 18,
"cf_clearance": true
},
"ts_managed": {
"outcome": "challenge",
"seconds": null,
"token": false
}
}
],
"profile": {
"setup": "Camoufox, defaults",
"network": "cdpfleet-resi Tier 1, sticky US",
"page": "scrapingcourse",
"first": {
"outcome": "cleared",
"seconds": 11,
"cf_clearance": true
},
"second": {
"outcome": "content",
"seconds": null,
"cf_clearance": true
}
}
}Takeaways
- On scrapingcourse.com's challenge the browser decided, not the network. On 2026-10-21 headless Chromium, headful Chrome and headful Edge stayed on the interstitial in every cell of every run: your own proxy, the sticky Tier 1 exit and Tier 4 alike. Camoufox got through by itself on at least one network in every run (in 1–18 s), the same page and the same exit. Chrome and Edge headful are real branded builds, so this is not only about headless tells (case 16): engine-level signals such as the TLS and HTTP/2 fingerprint (case 01) are part of what Cloudflare scores.
- nowsecure.nl let every setup in and still ran its check in the background. The page loaded as
contentfor nearly every cell, andcf_clearancewas issued wherever it loaded: Cloudflare can pass a visitor without showing the interstitial at all. One page passing says little about the next one: site owners set their own security level. - Turnstile in managed mode issued no token by itself, for any setup, on any network. Managed mode decides per visitor whether to show the checkbox; in 12 s none of the 10 setup/network cells got a token without a click, and nothing was clicked. The non-interactive widget on the same site issued a token in most cells, in 4–12 s; the misses (no token within 12 s) were mostly through your own rotating proxy, for every setup at least once.
- A returning visitor skips the check. Camoufox with a new
profilepassed scrapingcourse.com's interstitial in 6–18 s and gotcf_clearance; the next session with the same profile and the same sticky exit loaded the page ascontentstraight away, in every run.cf_clearanceis bound to the visitor (IP and browser), which is why the exit was kept sticky (case 40 has profiles in depth). - Cloudflare's own Turnstile demo can't tell browsers apart. Its demo pages use Cloudflare's published test sitekeys, which always pass (or always challenge) by design, so this case uses a test page with a real sitekey instead. These are dated snapshots of four public test pages: which vendor sits in front of a site is in case 62, what fingerprint test pages see per browser in case 63, and the signals themselves in What a page can read about your browser.