What goes into the hash
JA3 builds a comma-and-dash-delimited string from five Client Hello fields, in order:
TLSVersion,Ciphers,Extensions,EllipticCurves,PointFormatsFor example 771,4865-4866-4867-49195-...,0-23-65281-10-11-...,29-23-24,0, then MD5 of that string is the JA3. The decisive insight is that this captures the TLS library, not the application. Real Chrome offers a specific set of ciphers and extensions in a specific order baked into BoringSSL. Python's requests (OpenSSL), Go's crypto/tls, and Node each produce a different, recognizable Client Hello. So a scraper sending a flawless Chrome User-Agent over Python's TLS stack has a JA3 that screams "Python" - a contradiction the server sees before reading a single header.
JA3's weakness and why JA4 replaced it
JA3 has a real flaw: it is sensitive to field order, and modern Chrome deliberately randomizes the order of TLS extensions on every connection (part of the GREASE anti-ossification scheme). That means real Chrome produces a different JA3 hash per connection, so you cannot match against a single known-Chrome JA3 - the raw hash is unstable for exactly the clients you most want to allow.
The fix is JA4, which sorts the cipher and extension lists before hashing (so order randomization no longer changes the result), separates the hash into readable segments, and adds a transport/version prefix. Most vendors now compute JA4 (and the JA4+ suite including JA4H for HTTP/2), but JA3 remains widely deployed and is still the term most engineers recognize, so the two coexist.
Defeating JA3 means impersonating the TLS stack
You cannot fix a JA3 mismatch at the HTTP layer - it is computed below it. The only fix is to make your client emit a Client Hello byte-identical to a real browser. Options:
- curl-impersonate / curl_cffi - curl rebuilt against BoringSSL with Chrome's exact cipher/extension configuration.
- TLS-impersonating libraries - tls-client (Go), rnet/noble-tls, which ship per-browser Client Hello profiles.
- A real browser - Playwright/Puppeteer driving real Chrome naturally has Chrome's JA3/JA4.
The catch is coherence: matching JA3 is not enough if your HTTP/2 fingerprint or headers still look like a library. Vendors cross-check the TLS fingerprint against the HTTP/2 preface and the Client Hints - a request that is Chrome on JA3 but Go on HTTP/2 is still caught. Impersonation has to be end-to-end.
