Detection · Sep 25, 2026 · 6 min read
JA4 fingerprint: 1 TLS extension that split Chrome from a fork
What the JA4 TLS fingerprint reads before any script runs, why JA3 cannot follow Chrome, and the one extension that split our Chromium build from real Chrome.

In short
A server reads your TLS fingerprint before it sends a single byte of the page, so no script, no stealth plugin and no CDP call can touch it. We compare the JA4 of our own Chromium build with real Chrome on every engine version. On Chromium 150 the two strings were identical. On 151 they split by exactly one TLS extension, which Chrome sends only because Google switched it on for its users through a field trial.
What is a JA4 fingerprint?
A JA4 fingerprint is a short string that describes how a client opens a TLS connection, computed from the ClientHello message the client sends first. FoxIO maintains it as the successor of JA3, and its specification makes one decision that matters for Chrome: the extension list is "sorted (not in the order they appear)" before it is hashed, and GREASE values are ignored "anywhere it sees them".
JA3, the older format, joins five ClientHello fields in the order they appear, SSL version, ciphers, extensions, elliptic curves and point formats, and hashes the result with MD5. Salesforce no longer maintains it, and its repository points to FoxIO for the current work. Against Chrome, JA3 has a structural problem: Chrome shuffles its extension order on every connection, so the same browser produces a new JA3 each time.
| Connection | Extension order as sent (hex, GREASE included) |
|---|---|
| 1 | fafa, 002d, fe0d, 001b, 002b, 0005, 0023, ff01, 0010, 44cd, 0000, 0012, 000a, 000b, 0017, 0033, 000d, 7a7a |
| 2 | 3a3a, 000b, 0012, 44cd, 000d, 0017, 001b, 0010, 002b, ff01, 0023, 0000, fe0d, 0033, 000a, 002d, 0005, fafa |
| 3 | caca, 0023, 000d, fe0d, 001b, 002d, 002b, 0033, 000a, ff01, 0000, 44cd, 0005, 000b, 0010, 0012, 0017, 5a5a |
JA4 sorts these three lists into one and returns the same value for all three connections. That is why a comparison on JA4 can fail on a real difference and never on the shuffle.
Which parts of the ClientHello make up JA4?
JA4 has three parts separated by underscores, and the first part is readable without any decoding. Our Chromium 150 build produced t13d1516h2_8daaf6152771_806a8c22fdea, which breaks down like this:
tsays TLS over TCP; QUIC would beq.13is the TLS version, here TLS 1.3.dmeans the client sent a server name (SNI); a connection to a bare IP address showsi, which no real website ever sees.15is the number of cipher suites, GREASE removed.16is the number of extensions, GREASE removed.h2is the first and last character of the first ALPN value, here HTTP/2.8daaf6152771is the SHA-256 of the sorted cipher list, truncated to 12 characters.806a8c22fdeais the SHA-256 of the sorted extension list without SNI and ALPN, followed by the signature algorithms in the order sent, truncated the same way.
A single extension more or less changes the fifth field from 16 to 17 and changes the last hash completely, which is the difference we found.
Why did a fork differ from Chrome by one extension?
The difference came from a Chrome field trial, not from a patch of ours. Chrome downloads a variations seed that switches features on for a share of its users, and on 151 the study PqcBandwidthExperiment enables AddTLSServerHandshakePadding, which adds the TLS extension 0x12e0. We measured three states on the same host, three runs each, and the probe reads the raw ClientHello through a TCP listener in front of the TLS server rather than through CDP.
| Browser, date | JA4, first and last part | Sends 0x12e0 |
|---|---|---|
| our engine 150, 12 Aug | t13d1516h2 … 806a8c22fdea | no |
| real Chrome 150, 12 Aug | t13d1516h2 … 806a8c22fdea | no |
| our engine 151.0.7922.99, 25 Aug | t13d1516h2 … 806a8c22fdea | no |
| branded Chrome 151 with seed, 25 Aug | t13d1517h2 … c4845d6bf6c5 | yes |
The middle part, the cipher hash 8daaf6152771, was the same in all four rows. On 150 the trial had a minimum version of 151.0.7887.0, so leaving the extension out was correct. On 151 the study covers about 100 percent of the population in six enabled arms of roughly 17 percent each. Our 151 build therefore sent a handshake that matches Chrome 150 while its user agent says 151, and a server that knows the Chrome population can see that contradiction before the page loads.
We trust the probe because it passed three controls. It reproduces the two example hashes printed in the FoxIO specification. For a real Chrome 150 capture it computes the same hashes as the independent service tls.peet.ws. And Chrome 150 started with the field trial testing config sends one extra extension, 0xca34, and produces a different JA4 (t13d1517h2_…_cb7bf5808d99), so the comparison demonstrably fails when a real difference exists.
How does a proxy change what the server sees?
A proxy changes the TCP layer and leaves the TLS fingerprint alone. A terminating proxy, the kind residential and mobile providers run, opens its own TCP connection to the site, so the TCP fingerprint belongs to the proxy's device. The TLS ClientHello still travels end to end inside the tunnel, so the site reads the browser's JA4 no matter which exit carries it.
What is still open?
The 151 finding is measured, and the state of our current 154 engine is not. Three things are open on 22 September 2026:
- The shipped 154 build has not been measured against branded Chrome 154 with its variations seed. The study still applies to 154, the feature is off by default in the Chromium 154 source, and our build does not download a seed, so the gap most likely persists.
- Our pre-JavaScript gate compares the build against Chrome for Testing, which also runs without a seed, and therefore cannot see this gap.
- Whether the build should follow studies that reach 100 percent of users is a flag decision with its own measurement, and it is not made yet.
What should you check in your own automation stack?
Compare your browser's JA4 with the branded Chrome of the same version on the same machine, with its variations seed, and read the ClientHello outside the browser. CDP cannot help here: it hands back a normalised view of the request after TLS is done. If your tool drives a Chromium build of its own, the version string and the handshake have to move together, because a checker only needs the user agent and the extension count to find the mismatch. The engine page of the documentation describes how our build is versioned and updated.
Run it on your own machine
Seven days to try it with your own agents on your own sites. Starting the trial needs a card.