How TLS fingerprinting works
A TLS Client Hello carries a fixed-position list of fields — protocol version, supported cipher suites, named curves, point formats, signature algorithms, and the order of TLS extensions. Each combination is distinctive to the library that built the handshake. Python requests looks nothing like Chrome; Go net/http looks nothing like Firefox; curl looks like none of them. Anti-bot vendors hash these fields and compare the result to known browser baselines.
The hash is computed at the CDN edge, before any HTTP byte is parsed, before any HTML is served, before any JavaScript runs. This is why a perfect User-Agent spoof from requests still gets blocked — the TLS layer already failed.
JA3 vs JA4 — what changed in 2023
The first widely adopted fingerprint was JA3 (Salesforce, 2017): an MD5 hash of SSLVersion,Cipher,Extensions,EllipticCurve,EllipticCurvePointFormat. It worked for years but had two problems — TLS 1.3 GREASE values (random padding) made the hash unstable, and a single MD5 hid which field actually changed.
JA4+ (FoxIO, 2023) is the replacement. Instead of one MD5 it produces a structured fingerprint with separate components for the TLS Client Hello (JA4), the HTTP/2 client preface and SETTINGS frame (JA4H), the TLS server response (JA4S), and TCP-level timings (JA4T). Each component is independently inspectable, GREASE-stable, and human-readable.
| Fingerprint | What it covers | Status |
|---|---|---|
JA3 | TLS Client Hello (MD5) | Legacy — still seen on older WAFs |
JA4 | TLS Client Hello, GREASE-stable | Current standard at major vendors |
JA4H | HTTP/2 client preface + SETTINGS frame order | Required to pass Akamai, Cloudflare |
JA4S | Server-side TLS response fingerprint | Used by vendors to detect MITM proxies |
JA4T | TCP options + window size + timing | Rare — DataDome experimental |
If you are still targeting JA3-only on a 2026 site, your fingerprint will look correct in tls.peet.ws but block in production. Match JA4 + JA4H together.
HTTP/2 framing — the layer underneath TLS
Even if the TLS handshake matches Chrome exactly, the HTTP/2 connection that follows has its own fingerprint. Chrome sends a specific SETTINGS frame (HEADER_TABLE_SIZE=65536, INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144) followed by a WINDOW_UPDATE frame with a delta of 15663105. The frame order and exact values change per Chrome major version.
Go's net/http2 and Python's httpx use different SETTINGS values and different frame ordering. Vendors hash the SETTINGS frame body and compare. curl_cffi ships with the Chrome-matching values baked in; tls-client and noble-tls require manual configuration. This is the second-most-common reason a JA4-correct request still gets a 403.
QUIC and HTTP/3 — the next surface
Cloudflare and Google now serve a significant share of traffic over QUIC (HTTP/3 over UDP). QUIC has its own handshake, its own SETTINGS frame equivalent, and its own fingerprint surface — and almost no scraping library implements it yet. Most scrapers fall back to HTTP/2 by sending an Alt-Svc-incompatible header set, which is itself a signal.
For now QUIC-aware fingerprinting is rare in production blocks but expect this surface to matter by late 2026, particularly on Google properties and Cloudflare's premium tier. The mitigation is the same: pick a library that handshakes the way Chrome does end-to-end.
Why this matters for scraping
If your scraper sends a User-Agent header claiming to be Chrome 131 on macOS, but its JA4 hash matches Python urllib3, you flag faster than if you had not spoofed anything — because the mismatch itself is the signal. Header spoofing without TLS spoofing is the single most common rookie mistake in modern scraping.
JA4+ also covers JA4H (HTTP header order and content), JA4X (X.509 certs), and JA4T (TCP options + window). The picture an anti-bot builds extends well beyond the cipher list.
How scrapers defeat it
The fix is a TLS-impersonating HTTP client. curl_cffi wraps curl-impersonate, a patched build of curl that uses BoringSSL (Chrome's TLS library) with Chrome's exact cipher list and HTTP/2 SETTINGS frames. A single impersonate="chrome131" argument gives you a JA4 indistinguishable from real Chrome. tls-client (Go), noble-tls, and hrequests do the same in their respective ecosystems. Real browsers — Chrome, Firefox, Camoufox — speak real TLS by definition.
Keep impersonation profiles fresh. A Chrome 120 fingerprint in 2026 is itself suspicious because real users updated.
