JA3 vs JA4 TLS Fingerprinting Explained

Key takeaways

  • A TLS ClientHello is sent in plain text before encryption begins, and it lists your cipher suites, extensions, curves and ALPN protocols in an order your TLS library chose.
  • JA3 concatenates five ClientHello fields and MD5-hashes them. JA4 replaced it with a structured, partly human-readable string whose lists are sorted before hashing.
  • JA3 broke because Chrome started randomising its extension order — one browser began producing a different JA3 on every single connection.
  • Changing the User-Agent does nothing to your TLS fingerprint. Headers travel inside the tunnel the handshake already built; the fingerprint was sent and scored before your header existed.
  • A Python requests call inherits OpenSSL defaults that no shipping browser produces, which is why perfect browser headers on a Python client are still identified as a Python client.
  • The actual fix is a different TLS stackcurl_cffi, utls-based clients, or driving a real browser — not a header edit.
  • A proxy does not change your JA4 at all. The same script produces the same fingerprint over a datacenter, residential or mobile address.

There is a moment in almost every scraping project where someone copies their browser's headers into a script, runs it, and gets blocked anyway. The headers are perfect. The User-Agent is copied character for character from a real Chrome install. It changes nothing.

The reason is that the request was already identified before the first header was written. TLS fingerprinting reads the handshake — the message your client sends to start the encrypted connection, and that message describes your software in detail you never chose and cannot edit from the application layer.

This page explains what is in that message, how JA3 hashed it, why JA4 replaced it, and what actually changes the fingerprint. It is one layer of the full detection stack — the layer that catches most homemade automation.

What a TLS ClientHello actually reveals

A ClientHello is the first message your client sends, in plain text, before encryption begins. It advertises the TLS versions it supports, its cipher suite list, its extension list, its elliptic curves and point formats, and its ALPN protocols. Both the contents and the order are chosen by your TLS library, not by you.

None of that is secret — it is ordinary negotiation, readable because the two sides have not agreed on encryption yet. It is also extraordinarily specific: Chrome, Firefox, Safari, Go's standard library, OpenSSL, Java and .NET all offer different cipher lists, different extension sets, and different orderings.

The fields carrying most of the identifying weight:

Any middlebox on the path can read all of it, which is why edge networks fingerprint here: the check is free, it runs before request routing, and nothing above the transport can spoof it.

How JA3 builds a fingerprint

JA3, published by Salesforce in 2017, concatenates five fields from the ClientHello — TLS version, cipher suites, extensions, elliptic curves and curve point formats — into one decimal string, then takes an MD5 hash of it. The result is a 32-character identifier for the client software that produced the handshake.

The pre-hash string is five comma-separated groups, each an ordered list of decimal values joined by dashes.

TLSVersion,Ciphers,Extensions,EllipticCurves,PointFormats

771,4865-4866-4867-49195-49199,0-23-65281-10-11-35-16-5-13,29-23-24,0
                  │                        │
        cipher suites in the        extensions in the
        order the client sent       order the client sent

JA3 was genuinely useful for years: cheap to compute at the edge, compact enough to share between organisations, and specific enough to name an exact HTTP library or malware family. Server-side handshakes got the same treatment as JA3S.

Its weakness is visible in the diagram: the string is built in the order the client sent things, and MD5 turns any change into a completely unrelated hash. That assumption held right up until a browser vendor broke it.

Why JA3 stopped working

JA3 hashes the lists in the order they appeared, so any change to that order produces a completely different hash. In 2023 Chrome began randomising its ClientHello extension order deliberately, which meant a single browser generated a new JA3 on every connection. The fingerprint stopped identifying anything.

The change was not aimed at fingerprinting. It was anti-ossification: servers and middleboxes had started depending on Chrome's exact ordering, making the protocol impossible to evolve. Shuffling the order on every connection forces implementations to treat extensions as the unordered set the specification always said they were.

The side effect was immediate. Any blocklist or rate limit keyed to a JA3 hash now matched one connection from one Chrome install and never matched again. GREASE — deliberately meaningless values inserted into the lists for the same anti-ossification reason — had already added noise implementations had to strip by hand. Randomised ordering finished the job.

A fingerprint that changes on every request is not a fingerprint. It is a session identifier that nobody asked for.

What JA4 does differently

JA4 replaced the single opaque hash with a structured, human-readable string in three parts: a plain-text prefix describing the connection, a hash of the cipher list, and a hash of the extension list. Crucially, both lists are sorted before hashing, so randomised ordering no longer changes the result.

A JA4 string looks like this, and most of it can be read without any tooling:

t13d1516h2_8daaf6152771_02713d6af862
│││ ││ ││       │              │
│││ ││ ││       │              └─ hash of the sorted extension list
│││ ││ ││       │                 plus signature algorithms
│││ ││ ││       └─ hash of the sorted cipher suite list
│││ ││ │└─ ALPN: first and last character of the first offered protocol
│││ ││ └─ number of extensions
│││ │└─ number of cipher suites
│││ └─ SNI present (d = a domain name was sent, i = an IP)
││└─ TLS version
│└─ transport: t = TCP, q = QUIC, d = DTLS

Three design decisions matter, each fixing a specific JA3 failure:

JA4 is one member of a family that also covers TLS servers, HTTP clients, X.509 certificates, SSH sessions and — relevant on the TCP/IP fingerprinting page — the raw TCP layer, as JA4T.

Why a Python request is identifiable on sight

Python's requests library hands TLS to OpenSSL with its default configuration. That configuration offers a cipher list, extension set and curve list that no shipping browser offers, in an order no browser uses. It produces one small set of fingerprints shared by every Python script on the internet.

That last part is the problem. It is not that OpenSSL looks suspicious — it is that it looks like OpenSSL, and an enormous volume of scripted traffic looks identical. A fingerprint with no browser behind it and a large automated population behind it is trivially scored.

The same applies to every general-purpose HTTP client — Go's net/http, Java's HttpClient, Node's fetch, stock curl, wget. None of this requires a vendor to have profiled you; it requires them to have profiled the dozen most common libraries once.

Why changing the User-Agent does nothing

The User-Agent is an HTTP header. HTTP headers travel inside the encrypted tunnel that the TLS handshake creates, so by the time your header is written the fingerprint has already been sent, read and scored. You cannot edit a message that was transmitted several round trips ago.

Written as a sequence, the ordering problem is obvious:

1.  client → server   TCP SYN                  ← OS fingerprint readable here
2.  server → client   SYN/ACK
3.  client → server   TLS ClientHello          ← JA3/JA4 readable here, in plain text
4.  server → client   ServerHello, certificate
5.  ...               key exchange, Finished
6.  client → server   GET / HTTP/2             ← your User-Agent finally appears
                      user-agent: Mozilla/5.0...

Steps 1 and 3 happen regardless of what you put in step 6. Worse, a mismatched header is actively harmful: a User-Agent claiming Chrome on macOS arriving on an OpenSSL handshake is a contradiction. Without the edit you were an unidentified HTTP client; with it you are one pretending to be a browser.

It is the most common piece of bad advice in scraping tutorials, and worth stating without hedging: header spoofing cannot fix a handshake-layer identification.

What actually changes a TLS fingerprint

Only the TLS library changes a TLS fingerprint. Clients built for this — curl_cffi in Python, utls-based clients in Go, and the browser-impersonating curl forks they wrap — replace the handshake itself so the ClientHello matches a real browser build. A real browser, driven by automation, is the other option.

The realistic choices:

Two caveats. Impersonation targets go stale: browsers change handshakes across releases, so a client pinned to a two-year-old Chrome eventually announces itself as one. And matching the handshake while behaving like a script still fails — the fingerprint was one input.

The HTTP/2 fingerprint sitting right behind TLS

Matching a browser's TLS handshake and then speaking HTTP/2 like a library gets you caught one layer higher. The opening SETTINGS frame, the window update size, the priority information and the order of the pseudo-headers all differ between browser engines, and all of it is as fingerprintable as the ClientHello.

The pseudo-headers are the clearest example. HTTP/2 replaces the request line with four fields — :method, :authority, :scheme, :path, and browser engines emit them in different characteristic orders. HTTP libraries emit them in whatever order their implementation happens to build the frame.

The initial SETTINGS frame carries header table size, maximum concurrent streams and initial window size; browsers use distinctive, stable combinations. Tools that impersonate browser TLS generally patch these frames too — a good reason to use one rather than assembling your own handshake and losing at the next layer up.

What a proxy changes here

Nothing. A proxy forwards your bytes; it does not generate them. Your ClientHello is built by your TLS library on your machine and passes through the proxy unaltered, which is why an identical script gets an identical JA4 over a datacenter proxy, a residential proxy and a dedicated mobile one.

The mechanism is the same for both protocols people use. With HTTP CONNECT the proxy opens a TCP tunnel and copies bytes; with SOCKS5 it does the same after a short negotiation. Neither terminates TLS, so neither sees or rebuilds your handshake. That is a security property, not a limitation — a proxy that could rewrite your ClientHello would be one that decrypts your traffic.

The practical consequence is that TLS fingerprinting and proxy selection are separate purchases. A better address does not improve your handshake, and a fixed handshake does not rescue an address already classified. If your requests fail identically across every proxy you try, the address was never the variable — check the handshake before buying more IPs, as the detection overview argues at length.

How to check your own fingerprint

Point your client at a service that echoes back the fingerprint it observed, then point a real browser at the same service and compare. The two JA4 strings will differ in ways you can read directly, because JA4's prefix is plain text — cipher count, extension count and ALPN are visible without decoding anything.

What to compare, in order of how much each tells you:

Run it from the same machine, then again through your proxy. The fingerprint is identical both times — the point of this page, demonstrated in a minute.

The honest limits of TLS impersonation

Matching a browser's TLS fingerprint removes one signal from a list. It does not touch your TCP stack, your browser fingerprint, your behaviour or your account history, and a perfectly impersonated handshake arriving from an address classified as a datacenter is still a request from a datacenter.

It also introduces a new way to be inconsistent. A client impersonating Safari on macOS while running on a Linux server presents a macOS TLS fingerprint on a Linux TCP stack — a combination no real machine produces. That layer sits below TLS and cannot be reached from the application at all: TCP/IP fingerprinting.

The framing that survives contact with reality is the one this section is built on: every layer you fix moves your score by that layer's weight, and consistency between layers beats perfection in any one of them. If you are still in a challenge loop after fixing your handshake, look at the browser environment or the request rate before the network — CAPTCHA loops covers what that looks like from outside.

Frequently asked questions

What is the difference between JA3 and JA4?

JA3 is a single MD5 hash of five ClientHello fields in the order they appeared, so any reordering changes it. JA4 is a structured string in three parts — a readable prefix, a cipher hash and an extension hash — and it sorts the cipher and extension lists before hashing, so randomised ordering no longer changes the result.

Why did JA3 stop working?

Chrome began deliberately randomising the order of its ClientHello extensions, so a single browser produced a different JA3 hash on nearly every connection. A fingerprint that changes constantly cannot identify a client, and blocklists built on JA3 hashes became noise for exactly the browser that matters most.

Can I change my JA3 or JA4 fingerprint?

Only by changing the TLS library that builds the handshake. The fingerprint is a direct consequence of which cipher suites, extensions and curves your stack offers and in what order. No application-level setting, header, or proxy affects it — you need a client such as curl_cffi or a utls-based library, or a real browser.

Does changing my User-Agent change my TLS fingerprint?

No. The User-Agent is an HTTP header, and HTTP headers are sent inside the encrypted channel that the TLS handshake creates. The ClientHello was transmitted, observed and scored before the header was written. Editing it only creates a contradiction between what you claim to be and what your handshake showed.

Is a Python requests call detectable?

Immediately, at the handshake. It hands TLS to OpenSSL with defaults that no shipping browser produces, so the fingerprint identifies it as a scripting client before a single HTTP byte is sent. Adding browser-shaped headers does not change this and makes the mismatch more obvious, not less.

Does using a mobile proxy change my TLS fingerprint?

No. A proxy relays bytes your machine generated. Your ClientHello is built by your own TLS library and reaches the destination unchanged, so the same script produces the same JA4 over every proxy type. A proxy changes the source address and its reputation, and nothing in the handshake.

Dedicated mobile proxies, one dashboard

Real 4G/5G devices on US carrier SIMs. Sticky IP per customer, rotation on demand.

See plans