Cases / #73 · 2026-10-28 · Medium
Akamai on real sites: twelve browsers, three sites, one exit each
Three real sites behind Akamai Bot Manager, chosen from our sweep as easy, medium and hard, loaded once by each of twelve browsers and by a returning Camoufox profile, through one sticky residential exit per site, with the state of Akamai's _abck cookie. Dated 2026-10-28.
Google Chrome Microsoft Edge Brave Opera Yandex Browser Naver Whale Chromium Patchright Firefox WebKit Camoufox
Run on production on 2026-10-28: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Akamai's own site (case 67) shows how its Bot Manager behaves, but every real site sets its own rules. This case takes three real sites behind Akamai from our sweep of production sites, one that let headless Chrome in (easy), one that only Camoufox passed (medium) and one that stopped Camoufox too (hard), and loads each once with every browser the fleet offers, plus a returning Camoufox profile, through one sticky residential exit per site, as case 70 and case 71 did for Cloudflare and DataDome. It also reads Akamai's _abck cookie right after the page loads and 8 s later. 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 | Akamai's block page ("Access Denied", a reference number) or an error status with a short page is blocked; a press-and-hold or "verify you are human" page is challenge; anything else content. A connection Akamai resets (ERR_HTTP2_PROTOCOL_ERROR) counts as blocked. Nothing is clicked. |
context.cookies() | The _abck cookie's status field (the second ~-separated part) right after the page loads and 8 s later: -1 while Akamai's sensor data hasn't validated the visitor, 0 once it has. Read only, never set. |
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_AKAMAI | 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, read
_abck, wait 8 s and read it again. - 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_AKAMAI (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_AKAMAI.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 (/access denied|you don.t have permission to access|pardon our interruption|request unsuccessful|reference #/i.test(t) && len < 4000) return 'blocked';
if (/verify you are (a )?human|press (&|and) hold|are you a robot|just a moment|checking your browser/i.test(t) && len < 4000) return 'challenge';
return 'content';
})()`;
// Akamai's _abck cookie: the second ~-separated field is its status (-1 while the sensor data
// hasn't validated the visitor, 0 once it has). Read only, never written.
const abckStatus = (cookies) => { const c = cookies.find((x) => x.name === '_abck'); return c ? c.value.split('~')[1] ?? '?' : null; };
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, _abck right after it, then the
// page's state and _abck 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 (err) {
// Akamai refuses some clients by resetting the HTTP/2 connection: that's a block, not a network failure.
return { outcome: /ERR_HTTP2_PROTOCOL_ERROR/.test(err.message) ? 'blocked' : 'error', status: 0, abck_before: null, abck_after: null };
}
const abckBefore = abckStatus(await context.cookies());
await tab.waitForTimeout(8000);
let outcome = await tab.evaluate(STATE).catch(() => 'error');
if (outcome === 'content' && status >= 400) outcome = 'blocked';
return { outcome, status, abck_before: abckBefore, abck_after: abckStatus(await context.cookies()) };
} 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-c73${RUN}${site}-lifetime-30`;
const results = {};
for (const setup of SETUPS) {
let r = await visit(setup, url, proxy).catch(() => ({ outcome: 'error', status: 0, abck_before: null, abck_after: null }));
if (r.outcome === 'error') r = await visit(setup, url, proxy).catch(() => ({ outcome: 'error', status: 0, abck_before: null, abck_after: null }));
results[setup[0]] = r;
}
// A returning visitor: Camoufox with a profile, a first visit and then a second session.
const name = `case73-${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, abck_before: null, abck_after: null }));
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_AKAMAI (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_AKAMAI"].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 (/access denied|you don.t have permission to access|pardon our interruption|request unsuccessful|reference #/i.test(t) && len < 4000) return 'blocked';
if (/verify you are (a )?human|press (&|and) hold|are you a robot|just a moment|checking your browser/i.test(t) && len < 4000) return 'challenge';
return 'content';
})()"""
NONE = {"outcome": "error", "status": 0, "abck_before": None, "abck_after": None}
# Akamai's _abck cookie: the second ~-separated field is its status (-1 while the sensor data
# hasn't validated the visitor, 0 once it has). Read only, never written.
def abck_status(cookies):
c = next((x for x in cookies if x["name"] == "_abck"), None)
if not c:
return None
parts = c["value"].split("~")
return parts[1] if len(parts) > 1 else "?"
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, _abck right after it, then the
# page's state and _abck 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 as err:
# Akamai refuses some clients by resetting the HTTP/2 connection: a block, not a network failure.
return {**NONE, "outcome": "blocked" if "ERR_HTTP2_PROTOCOL_ERROR" in str(err) else "error"}
abck_before = abck_status(await context.cookies())
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, "abck_before": abck_before, "abck_after": abck_status(await context.cookies())}
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 dict(NONE)
# 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-c73{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"case73-{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_AKAMAI (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 (/access denied|you don.t have permission to access|pardon our interruption|request unsuccessful|reference #/i.test(t) && len < 4000) return 'blocked';
if (/verify you are (a )?human|press (&|and) hold|are you a robot|just a moment|checking your browser/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, String before, String after) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("outcome", outcome);
m.put("status", status);
m.put("abck_before", before);
m.put("abck_after", after);
return m;
}
// Akamai's _abck cookie: the second ~-separated field is its status (-1 while the sensor data
// hasn't validated the visitor, 0 once it has). Read only, never written.
static String abckStatus(BrowserContext context) {
for (com.microsoft.playwright.options.Cookie c : context.cookies()) {
if ("_abck".equals(c.name)) {
String[] parts = c.value.split("~");
return parts.length > 1 ? parts[1] : "?";
}
}
return null;
}
// One session, one load: the status of the page's own response, _abck right after it, then the
// page's state and _abck 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) {
// Akamai refuses some clients by resetting the HTTP/2 connection: a block, not a network failure.
return cell(String.valueOf(e.getMessage()).contains("ERR_HTTP2_PROTOCOL_ERROR") ? "blocked" : "error", 0, null, null);
}
String before = abckStatus(context);
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, before, abckStatus(context));
} finally {
browser.close();
}
} catch (Exception e) {
return cell("error", 0, null, null);
}
}
// 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-c73" + 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 = "case73-" + 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_AKAMAI").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_AKAMAI (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_AKAMAI")!.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 (/access denied|you don.t have permission to access|pardon our interruption|request unsuccessful|reference #/i.test(t) && len < 4000) return 'blocked';
if (/verify you are (a )?human|press (&|and) hold|are you a robot|just a moment|checking your browser/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, string? before = null, string? after = null) =>
new() { ["outcome"] = outcome, ["status"] = status, ["abck_before"] = before, ["abck_after"] = after };
// Akamai's _abck cookie: the second ~-separated field is its status (-1 while the sensor data
// hasn't validated the visitor, 0 once it has). Read only, never written.
async Task<string?> AbckStatus(IBrowserContext context)
{
var c = (await context.CookiesAsync()).FirstOrDefault(x => x.Name == "_abck");
if (c == null) return null;
var parts = c.Value.Split('~');
return parts.Length > 1 ? parts[1] : "?";
}
// One session, one load: the status of the page's own response, _abck right after it, then the
// page's state and _abck 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 e)
{
// Akamai refuses some clients by resetting the HTTP/2 connection: a block, not a network failure.
return Cell(e.Message.Contains("ERR_HTTP2_PROTOCOL_ERROR") ? "blocked" : "error", 0);
}
var before = await AbckStatus(context);
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, before, await AbckStatus(context));
}
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-c73{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 = $"case73-{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_AKAMAI (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 (/access denied|you don.t have permission to access|pardon our interruption|request unsuccessful|reference #/i.test(t) && len < 4000) return 'blocked';
if (/verify you are (a )?human|press (&|and) hold|are you a robot|just a moment|checking your browser/i.test(t) && len < 4000) return 'challenge';
return 'content';
})()`
type cell struct {
Outcome string `json:"outcome"`
Status int `json:"status"`
AbckBefore *string `json:"abck_before"`
AbckAfter *string `json:"abck_after"`
}
// Akamai's _abck cookie: the second ~-separated field is its status (-1 while the sensor data
// hasn't validated the visitor, 0 once it has). Read only, never written.
func abckStatus(ctx playwright.BrowserContext) *string {
cookies, err := ctx.Cookies()
if err != nil {
return nil
}
for _, c := range cookies {
if c.Name == "_abck" {
v := "?"
if parts := strings.Split(c.Value, "~"); len(parts) > 1 {
v = parts[1]
}
return &v
}
}
return nil
}
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, _abck right after it, then the
// page's state and _abck 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{Outcome: "error"}
}
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{Outcome: "error"}
}
defer browser.Close()
var ctx playwright.BrowserContext
if profile != "" {
ctx = browser.Contexts()[0]
} else if ctx, err = browser.NewContext(); err != nil {
return cell{Outcome: "error"}
}
tab, err := ctx.NewPage()
if err != nil {
return cell{Outcome: "error"}
}
res, err := tab.Goto(url, playwright.PageGotoOptions{WaitUntil: playwright.WaitUntilStateDomcontentloaded, Timeout: playwright.Float(45000)})
if err != nil {
// Akamai refuses some clients by resetting the HTTP/2 connection: a block, not a network failure.
if strings.Contains(err.Error(), "ERR_HTTP2_PROTOCOL_ERROR") {
return cell{Outcome: "blocked"}
}
return cell{Outcome: "error"}
}
status := 0
if res != nil {
status = res.Status()
}
before := abckStatus(ctx)
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, before, abckStatus(ctx)}
}
// 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-c73" + 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 := "case73-" + 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_AKAMAI"), ",")
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 bank | Site B, a home-improvement retailer | Site C, a game-key marketplace | Measured |
|---|---|---|---|---|
| Chrome, headful | content (200) | error | blocked (403) · _abck -1 | 2026-10-28 |
| Chrome, headless | content (200) | blocked (200) | blocked | 2026-10-28 |
| Edge, headful | content (200) | content (200) · _abck -1 | blocked (200) · _abck -1 | 2026-10-28 |
| Brave, headful | content (200) | content (200) · _abck -1 | content (200) · _abck -1 | 2026-10-28 |
| Opera, headful | content (200) | content (200) · _abck -1 | content (200) · _abck -1 | 2026-10-28 |
| Yandex, headful | content (200) | blocked (200) | blocked (200) · _abck -1 | 2026-10-28 |
| Whale, headful | content (200) | content (200) · _abck -1 | blocked (200) · _abck -1 | 2026-10-28 |
| Chromium, headless | content (200) | blocked (200) | blocked | 2026-10-28 |
| Patchright, headful | content (200) | content (200) · _abck -1 | blocked (403) · _abck -1 | 2026-10-28 |
| Firefox, headful | content (200) | content (200) · _abck -1 | blocked (200) · _abck -1 | 2026-10-28 |
| WebKit, headful | content (200) | content (200) · _abck -1 | blocked (200) · _abck -1 | 2026-10-28 |
| Camoufox, headful | content (200) | content (200) · _abck -1 | content (200) · _abck -1 | 2026-10-28 |
| Camoufox, returning profile (2nd visit) | content (200) | content (200) · _abck -1 | content (200) · _abck -1 | 2026-10-28 |
From the Node.js run on 2026-10-28. 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-28",
"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,
"abck_before": null,
"abck_after": null
},
"chrome_headless": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"edge": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"brave": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"opera": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"yandex": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"whale": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"chromium_headless": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"patchright": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"firefox": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"webkit": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"camoufox": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
},
"camoufox_returning": {
"outcome": "content",
"status": 200,
"abck_before": null,
"abck_after": null
}
}
},
{
"site": "B",
"results": {
"chrome_headful": {
"outcome": "error",
"status": 0,
"abck_before": null,
"abck_after": null
},
"chrome_headless": {
"outcome": "blocked",
"status": 200,
"abck_before": null,
"abck_after": null
},
"edge": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"brave": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"opera": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"yandex": {
"outcome": "blocked",
"status": 200,
"abck_before": null,
"abck_after": null
},
"whale": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"chromium_headless": {
"outcome": "blocked",
"status": 200,
"abck_before": null,
"abck_after": null
},
"patchright": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"firefox": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"webkit": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"camoufox": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"camoufox_returning": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
}
}
},
{
"site": "C",
"results": {
"chrome_headful": {
"outcome": "blocked",
"status": 403,
"abck_before": "-1",
"abck_after": "-1"
},
"chrome_headless": {
"outcome": "blocked",
"status": 0,
"abck_before": null,
"abck_after": null
},
"edge": {
"outcome": "blocked",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"brave": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"opera": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"yandex": {
"outcome": "blocked",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"whale": {
"outcome": "blocked",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"chromium_headless": {
"outcome": "blocked",
"status": 0,
"abck_before": null,
"abck_after": null
},
"patchright": {
"outcome": "blocked",
"status": 403,
"abck_before": "-1",
"abck_after": "-1"
},
"firefox": {
"outcome": "blocked",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"webkit": {
"outcome": "blocked",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"camoufox": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
},
"camoufox_returning": {
"outcome": "content",
"status": 200,
"abck_before": "-1",
"abck_after": "-1"
}
}
}
]
}Takeaways
- Headless was refused on both protected sites, before Akamai even set its cookie. On 2026-10-28 headless Chrome and headless Chromium got Akamai's block page on site B in all five runs, with no
_abckcookie at all, and on site C Akamai reset the connection (ERR_HTTP2_PROTOCOL_ERROR) every time. Yandex, though headful, was blocked on site B the same way in every run. - On the hardest site the winners were Brave and Opera, not Chrome. Site C let Brave and Opera in on all five runs and blocked headful Chrome, Edge, Whale, Yandex and Patchright in every run, the reverse of DataDome's sites, where headful Chrome was the one setup let in everywhere (case 71). Firefox and WebKit were blocked on site C too.
- A returning Camoufox profile helped on Akamai. On site C a fresh Camoufox got the page in two of five runs; the second visit with a saved profile got it in all five. On DataDome's sites a returning profile changed nothing; on Cloudflare's it helped, as here.
_abcknever validated, and it didn't decide the page. Wherever Akamai set_abck, its status read-1right after the page loaded and still-18 s later, in every cell of every run: on site B the page was served with it unvalidated, on site C most setups were blocked with the same-1. Akamai's own site behaved the same way (case 67).- Site B let every other headful browser in. Headful Chrome, Edge, Brave, Opera, Whale, Patchright, Firefox, WebKit and Camoufox (fresh and returning) got site B in every run (one network error aside).
- Site A let everyone in and set no
_abckcookie on its home page: a site can use Akamai for some pages or actions without checking its front door. The overview across vendors is /bot-protection; Cloudflare's and DataDome's real sites are case 70 and case 71.