How TLS fingerprinting works
The first message of a TLS handshake is the Client Hello. It lists, in a fixed order, the protocol version, the cipher suites the client supports, named curves, point formats, signature algorithms, and the TLS extensions it sends. The catch: each software library fills in this list differently, so the combination acts like a signature for the tool that built it. 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.
This hash is computed at the CDN edge, before any HTTP byte is parsed, before any HTML is served, before any JavaScript runs. That is why a perfect User-Agent spoof from requests still gets blocked — the TLS layer already gave the game away.
JA3 vs JA4 — what changed in 2023
The first widely adopted fingerprint was JA3 (Salesforce, 2017): an MD5 hash (a short fixed-length code) of SSLVersion,Cipher,Extensions,EllipticCurve,EllipticCurvePointFormat. It worked for years but had two problems — TLS 1.3 added GREASE values (random padding browsers insert on purpose), which made the hash change run to run, and a single MD5 hid which field had actually changed.
JA4+ (FoxIO, 2023) is the replacement. Instead of one MD5 it produces a structured fingerprint with separate parts: 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 part can be read on its own, ignores GREASE so it stays stable, and is human-readable rather than one opaque hash.
| 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 can look correct in the testing tool tls.peet.ws yet still block in production. Match JA4 + JA4H together.
HTTP/2 framing — the layer underneath TLS
Even if your TLS handshake matches Chrome exactly, the HTTP/2 connection that runs on top of it has its own fingerprint. When Chrome opens an HTTP/2 connection it sends a specific SETTINGS frame — the opening message that declares connection parameters (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 with each Chrome major version.
Go's net/http2 and Python's httpx use different SETTINGS values and a different frame order. Vendors hash that SETTINGS frame and compare. curl_cffi ships with the Chrome-matching values baked in; tls-client and noble-tls need them set by hand. This is the second-most-common reason a request with a correct JA4 still gets a 403.
QUIC and HTTP/3 — the next surface
Cloudflare and Google now serve a large share of traffic over QUIC, which is HTTP/3 running on UDP instead of TCP. QUIC has its own handshake, its own SETTINGS-frame equivalent, and therefore its own fingerprint surface — and almost no scraping library supports it yet. Most scrapers quietly fall back to HTTP/2 by sending an Alt-Svc-incompatible header set, and that fallback is itself a signal.
For now QUIC-aware fingerprinting rarely shows up in production blocks, but expect it to matter by late 2026, especially on Google properties and Cloudflare's premium tier. The fix is the same as everywhere else: pick a library that completes the handshake the way Chrome does, end to end.
Why this matters for scraping
Say your scraper sends a User-Agent header claiming to be Chrome 131 on macOS, but its JA4 hash matches Python urllib3. You now flag faster than if you had spoofed nothing at all — because the mismatch between what you claim and what your TLS shows is the signal. Spoofing headers without also spoofing TLS is the single most common rookie mistake in modern scraping.
JA4+ also covers JA4H (HTTP header order and content), JA4X (X.509 certificates, the certificates that prove identity in TLS), and JA4T (TCP options + window). The profile an anti-bot builds reaches well beyond the cipher list.
How clients match a browser's TLS
To present a TLS handshake consistent with a real browser, scrapers use an HTTP client that reproduces that browser's TLS. curl_cffi wraps curl-impersonate, a patched build of curl that uses BoringSSL (Chrome's own TLS library) with Chrome's exact cipher list and HTTP/2 SETTINGS frames. A single impersonate="chrome131" argument gives you a JA4 that is indistinguishable from real Chrome. tls-client (Go), noble-tls, and hrequests do the same in their own ecosystems. Real browsers — Chrome, Firefox, Camoufox — speak real TLS by definition, so they pass without any tricks.
Keep your impersonation profiles current. A Chrome 120 fingerprint in 2026 is itself suspicious, because real users have long since updated.
