TLS Fingerprinting with JA3 and JA4: Why Proxies Alone Don't Hide You
A proxy rewrites the IP address on the outside of a packet. It does not change the bytes inside the TLS ClientHello. That distinction is the entire business model of modern anti-bot vendors: if the ClientHello produced by your HTTP client is distinguishable from the ClientHello a real Chrome browser produces, an operator can block you without ever looking at your source IP. This post walks through how JA3 and JA4 work at the byte level, what leaks through a proxy, and the countermeasures engineering teams use for legitimate testing and competitive intelligence.
The ClientHello is a Fingerprint
When a TLS 1.2 or 1.3 session begins (RFC 8446, Section 4.1.2), the client sends a ClientHello message containing the TLS version, a list of supported cipher suites, a list of supported extensions, the elliptic curves it supports, and the signature algorithms it accepts. None of these fields are required to be in any particular order. Chrome orders them one way. Firefox orders them another way. Python's requests library, which wraps urllib3, which wraps OpenSSL, orders them a third way. Go's crypto/tls package orders them a fourth way.
Because these orderings are implementation-specific and stable within a version, you can hash them and get a fingerprint that identifies the TLS stack in use. That is JA3.
JA3: The Original Hash
JA3 was introduced by Salesforce engineers in 2017. The construction is deliberately simple. You concatenate five fields from the ClientHello, separated by commas:
SSLVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats
Within each field, values are hyphen-separated in the order they appear in the ClientHello. You then take the MD5 of the resulting ASCII string. A real Chrome 120 on macOS produces a JA3 string that looks approximately like this:
771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0
Hashed with MD5, this collapses to a 32-character hex string such as cd08e31494f9531f560d64c695473da9. Python requests on the same machine produces an entirely different string and hash. An anti-bot vendor maintains a block list of hashes associated with common automation libraries, and a match is a near-certain bot signal regardless of the residential IP in front of it.
Why JA3 Started to Fail
JA3 has two practical problems. First, the cipher and extension lists changed frequently as browsers added post-quantum candidates, deprecated weak ciphers, and shipped TLS 1.3 features. Chrome's JA3 hash shifts roughly every major release, which means defenders have to re-learn the "good" hashes constantly. Second, Chrome introduced GREASE values (RFC 8701) which randomly inject unknown cipher and extension identifiers into the ClientHello to prevent protocol ossification. JA3 strips GREASE before hashing, but the original specification was inconsistent across implementations.
More damaging, JA3 ignores the order of extensions in TLS 1.3, and it ignores ALPN, which carries HTTP/2 negotiation. Two clients with identical JA3 hashes can still be trivially distinguishable at the ALPN layer.
JA4: The 2023 Rewrite
JA4, published by FoxIO in 2023, addresses these gaps. Instead of a single MD5 hash, JA4 produces a structured fingerprint made of three components separated by underscores, for example t13d1516h2_8daaf6152771_02713d6af862. The three parts encode:
- Prefix: TLS version (
t13for 1.3), SNI presence (dfor domain), cipher count (15), extension count (16), ALPN value (h2for HTTP/2). - Cipher hash: SHA-256 of sorted ciphers, truncated to 12 hex characters.
- Extension hash: SHA-256 of sorted extensions plus signature algorithms, truncated.
The key change is that JA4 sorts the cipher and extension lists before hashing. This means a Chrome update that reorders extensions does not invalidate the fingerprint. JA4 also has sibling fingerprints: JA4H for HTTP headers, JA4S for server response, JA4X for X.509 certificates. An anti-bot vendor can stack them and drop a request if any single component looks wrong.
What Leaks Through Your Proxy
A residential or ISP proxy operates at layer 4. Traffic arrives at the proxy over a TCP connection, and the proxy opens a new TCP connection to the upstream target. For HTTPS, the client uses HTTP CONNECT to ask the proxy to tunnel raw TCP bytes. From the proxy's perspective, the TLS handshake is opaque: it ferries the same bytes in both directions.
This means the ClientHello that reaches the origin is byte-identical to the one your client produced. Your residential IP in Chicago is doing nothing to hide the fact that your TLS stack is Python 3.11 with OpenSSL 3.0.2. Cloudflare, Akamai, PerimeterX, and DataDome all compute JA3 and JA4 on arrival. An IP from a residential ISP combined with a Python JA3 hash is a higher-confidence bot signal than either alone.
Countermeasures for Legitimate Testing
Engineering teams running QA, competitive intelligence, or brand monitoring need TLS stacks that match the browsers their users actually run. Four approaches are in use in April 2026.
curl-impersonate
Lexi Fox's curl-impersonate project patches curl and the underlying BoringSSL library to produce ClientHellos byte-identical to Chrome, Firefox, Edge, and Safari. You invoke it as curl_chrome120 https://target.example and the resulting JA3 matches a genuine Chrome 120 installation. The project tracks browser releases roughly within two months of a new stable channel.
utls in Go
Refraction Networking's utls library exposes a lower-level handshake API than Go's standard crypto/tls. It ships with preset ClientHello specifications for common browsers and lets you register custom specs. A production crawler can dial a target with utls.UClient(conn, config, utls.HelloChrome_120) and produce a handshake indistinguishable from Chrome on the wire.
Headless Browsers
For targets that inspect JA4H (HTTP layer) alongside JA4, even a perfect TLS impersonation is not enough. The HTTP/2 settings frame, header ordering, and HPACK dynamic table state must also match. Running the actual browser via Playwright or Puppeteer solves this at the cost of roughly 100 MB of RAM per session and 200-500 ms of startup latency.
Node.js with tls-client
The tls-client Node package wraps a Go-based HTTP client using utls underneath and exposes it to JavaScript. It is the fastest way to get browser-accurate TLS from a Node scraping pipeline without pulling in a full headless browser.
A Concrete Benchmark
Hex Proxies internal testing in April 2026 ran 10,000 requests against a representative Cloudflare-protected endpoint from the same residential IP pool, varying only the client TLS stack. Python requests achieved a 12.3 percent success rate. Node axios achieved 14.1 percent. curl-impersonate targeting Chrome 121 achieved 87.9 percent. Playwright with stock Chromium achieved 94.4 percent. The same residential IPs, the same target, the same rate. The differentiator was the ClientHello.
Operational Guidance
If you are running any kind of automated HTTP client against a modern anti-bot-protected endpoint, your TLS stack matters more than your IP. A common and expensive mistake is to upgrade from datacenter proxies to residential proxies expecting success rates to jump, when the real bottleneck is a JA4 mismatch. Measure your current JA3 and JA4 fingerprints against tls.peet.ws or a similar reflection service first. If they do not match the browser you are claiming to be in your User-Agent header, fix that before buying more IPs.
For teams running competitive intelligence workloads, pair ISP proxies with curl-impersonate or utls. The ISP IP provides the network reputation and the impersonation library provides the TLS reputation. Both are necessary, neither is sufficient.
What Comes Next
TLS fingerprinting is not a solved problem for either side. Cloudflare has published research on passive fingerprinting of QUIC initial packets, which would extend JA4-style analysis to HTTP/3. Google has experimented with Encrypted Client Hello (ECH, draft-ietf-tls-esni-18), which would move the SNI and extension list inside an encrypted envelope and blunt JA4 entirely. ECH deployment is gated on widespread HTTPS DNS record adoption and is still uneven in April 2026.
In the meantime, assume every request is fingerprinted at the TLS layer the moment it lands. Your proxy choice determines whether you look like a human neighborhood. Your TLS stack determines whether you look like a human browser. You need both.