cdpfleet Docs GitHub Dashboard

Cases / #1 · 2026-09-30 · Medium

What your browser says before it says anything: TLS and HTTP/2 fingerprints

Five engines, one endpoint (tls.peet.ws), and the fingerprints anti-bot systems read before your page even loads.

Google Chrome Microsoft Edge Firefox Camoufox WebKit

Run on production on 2026-09-30: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

Anti-bot systems don't wait for JavaScript. The first thing a server sees is the TLS ClientHello (cipher suites, extensions, their order) and, on HTTP/2, the SETTINGS and WINDOW_UPDATE frames. Those are condensed into fingerprints — JA3/JA4 for TLS, the Akamai HTTP/2 fingerprint for h2 — and compared with what the user agent claims to be. A Chrome user agent over a Python-requests TLS stack is flagged instantly. So: what does each cdpfleet engine actually send?

What we used, and why

WhatWhy
chrome, edge, firefox, camoufox, webkitOne of each engine family, to compare Chromium, Gecko and WebKit network stacks.
proxyEvery session needs one; fingerprints are made by the browser, not the proxy, so any proxy works.
headless: trueTLS and HTTP/2 don't change between headless and headful — the cheapest mode (1 thread) is enough.
page.goto(tls.peet.ws/api/all)The browser itself makes the request. page.request / APIRequest would go out through Playwright's own HTTP client, with a different fingerprint.

How it works

  1. Launch each browser with a proxy.
  2. Navigate to https://tls.peet.ws/api/all and read the JSON the page returns.
  3. Keep the user agent, HTTP version, JA4, JA3 hash, peetprint and Akamai HTTP/2 fingerprint.
  4. Close the browser — that ends the session and the billing.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL (http://user:pass@host:port)
import { chromium, firefox, webkit } from 'playwright';

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

// One browser per engine family. `family` is the Playwright client that speaks to it.
const BROWSERS = [
  { name: 'chrome', family: chromium },
  { name: 'edge', family: chromium },
  { name: 'firefox', family: firefox },
  { name: 'camoufox', family: firefox },
  { name: 'webkit', family: webkit },
];

async function launch(name, options) {
  const res = await fetch(`https://starter.cdpfleet.com/${name}/session`, {
    method: 'POST',
    headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
    body: JSON.stringify(options),
  });
  if (!res.ok) throw new Error(`launch ${name}: ${res.status} ${await res.text()}`);
  return res.json();
}

// Navigate the browser itself to tls.peet.ws: the JSON it returns describes the TLS
// ClientHello and HTTP/2 frames this very browser sent. (page.request would go out from
// Playwright's own HTTP client instead, with a different fingerprint.)
// A residential exit occasionally times out: one retry, in a fresh tab.
async function fingerprint(browser) {
  for (let attempt = 1; ; attempt++) {
    try {
      const page = await browser.newPage();
      return await (await page.goto('https://tls.peet.ws/api/all', { timeout: 30000 })).json();
    } catch (err) {
      if (attempt === 2) throw err;
    }
  }
}

const rows = [];
for (const b of BROWSERS) {
  const session = await launch(b.name, { proxy: PROXY, headless: true });
  const browser = await b.family.connect(session.wsUrl, { headers: { 'x-api-key': KEY } });
  try {
    const fp = await fingerprint(browser);
    rows.push({
      browser: b.name,
      version: browser.version(),
      user_agent: fp.user_agent,
      http_version: fp.http_version,
      ja4: fp.tls.ja4,
      ja3_hash: fp.tls.ja3_hash,
      peetprint_hash: fp.tls.peetprint_hash,
      akamai_h2: fp.http2?.akamai_fingerprint ?? null,
      akamai_h2_hash: fp.http2?.akamai_fingerprint_hash ?? null,
      cipher_suites: fp.tls.ciphers.length,
      extensions: fp.tls.extensions.length,
    });
  } finally {
    await browser.close(); // ends the session and stops billing
  }
}
console.log(JSON.stringify(rows, null, 2));

What we got

BrowserVersionHTTPJA4Akamai h2 hashCiphersExtensions
chrome153.0.8010.36h2t13d1517h2_8daaf6152771_cb7bf5808d9952d84b11737d980aef856699f885ca861619
edge152.0.4191.66h2t13d1516h2_8daaf6152771_806a8c22fdea52d84b11737d980aef856699f885ca861618
firefox150.0.2h2t13d1617h2_86a278354501_3cbfd9057e0d6ea73faa8fc5aac76bded7bd238f64331617
camoufox152.0.4-beta.30h2t13d1617h2_86a278354501_3cbfd9057e0d6ea73faa8fc5aac76bded7bd238f64331617
webkit26.4HTTP/1.1t13d2913h1_723694b0fccc_5671b5df5029—2913

From the Node.js run on 2026-09-30. 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)
[
  {
    "browser": "chrome",
    "version": "153.0.8010.36",
    "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/203.0.113.1 Safari/537.36",
    "http_version": "h2",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "ja3_hash": "1dadc3d8371c7cd23b73baca6ce4a14e",
    "peetprint_hash": "fc97c1cdfb1409c9a9326c1b726d1dee",
    "akamai_h2": "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86",
    "cipher_suites": 16,
    "extensions": 19
  },
  {
    "browser": "edge",
    "version": "152.0.4191.66",
    "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/203.0.113.2 Safari/537.36 Edg/203.0.113.2",
    "http_version": "h2",
    "ja4": "t13d1516h2_8daaf6152771_806a8c22fdea",
    "ja3_hash": "8aed6551da98b7685768242b17d63530",
    "peetprint_hash": "8f568b2a409b82d0ec24cb84bf3bc3fc",
    "akamai_h2": "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86",
    "cipher_suites": 16,
    "extensions": 18
  },
  {
    "browser": "firefox",
    "version": "150.0.2",
    "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0",
    "http_version": "h2",
    "ja4": "t13d1617h2_86a278354501_3cbfd9057e0d",
    "ja3_hash": "6447ab086255d194909d4013b1a89e87",
    "peetprint_hash": "fd4547eeb41f073156b7bc8125a79a3c",
    "akamai_h2": "1:65536;2:0;4:131072;5:16384|12517377|0|m,p,a,s",
    "akamai_h2_hash": "6ea73faa8fc5aac76bded7bd238f6433",
    "cipher_suites": 16,
    "extensions": 17
  },
  {
    "browser": "camoufox",
    "version": "152.0.4-beta.30",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
    "http_version": "h2",
    "ja4": "t13d1617h2_86a278354501_3cbfd9057e0d",
    "ja3_hash": "6447ab086255d194909d4013b1a89e87",
    "peetprint_hash": "fd4547eeb41f073156b7bc8125a79a3c",
    "akamai_h2": "1:65536;2:0;4:131072;5:16384|12517377|0|m,p,a,s",
    "akamai_h2_hash": "6ea73faa8fc5aac76bded7bd238f6433",
    "cipher_suites": 16,
    "extensions": 17
  },
  {
    "browser": "webkit",
    "version": "26.4",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.4 Safari/605.1.15",
    "http_version": "HTTP/1.1",
    "ja4": "t13d2913h1_723694b0fccc_5671b5df5029",
    "ja3_hash": "2cbcec37dfff29e4c6a34dd35ebe1d70",
    "peetprint_hash": "e2a4f6ec1c75767498bc87d934bfccec",
    "akamai_h2": null,
    "akamai_h2_hash": null,
    "cipher_suites": 29,
    "extensions": 13
  }
]

Takeaways