What goes into the hash
JA3 builds a comma-and-dash-delimited string from five Client Hello fields, always in this order:
TLSVersion,Ciphers,Extensions,EllipticCurves,PointFormatsA real example looks like 771,4865-4866-4867-49195-...,0-23-65281-10-11-...,29-23-24,0, and the MD5 of that string is the JA3 fingerprint. The key idea 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 (the encryption library Chrome ships with). Python's requests (which uses OpenSSL), Go's crypto/tls, and Node each produce a different, recognizable Client Hello. So a scraper that sends a flawless Chrome User-Agent over Python's TLS stack ends up with a JA3 that screams "Python" - a contradiction the server spots before reading a single header.
JA3's weakness and why JA4 replaced it
JA3 has a real flaw: it depends on the order of the fields, and modern Chrome deliberately shuffles the order of its TLS extensions on every connection (part of GREASE, a scheme that keeps servers from hard-coding assumptions about clients). Because of that shuffling, real Chrome produces a different JA3 hash each time it connects, so you cannot match it against one 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 reshuffling no longer changes the result), splits the hash into readable segments, and adds a transport/version prefix. Most vendors now compute JA4 (and the wider JA4+ suite, including JA4H for HTTP/2), but JA3 is still widely deployed and remains the term most engineers recognize, so the two coexist.
Why a JA3 match depends on the TLS stack
A JA3 value is determined below the HTTP layer, so it reflects the underlying TLS client rather than anything set in the application. A client's JA3 matches a given browser only when it sends the same Client Hello that browser's TLS library produces. In practice this is why certain tools share a browser's JA3:
- curl-impersonate / curl_cffi - curl built against BoringSSL with Chrome's cipher/extension configuration.
- TLS-aware libraries - tls-client (Go), rnet/noble-tls, which ship per-browser Client Hello profiles.
- A real browser - Playwright/Puppeteer driving real Chrome naturally produces Chrome's JA3/JA4.
Coherence is the broader point: a JA3 that matches a browser is only one signal. Vendors cross-check the TLS fingerprint against the HTTP/2 preface and the Client Hints, so a request whose TLS layer looks like Chrome but whose HTTP/2 layer looks like a library is internally inconsistent. The signals only agree end-to-end on a genuine browser stack.
