Scalebrowser

JA4 fingerprint: 1 TLS extension tells 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.

Davide Valli · Founder, Scalebrowser

Published Sep 25, 2026 · 7 min read

Short answer

A JA4 fingerprint turns the TLS ClientHello into one string before any page script runs: transport, TLS version, SNI, cipher and extension counts, ALPN, then hashes of the sorted cipher and extension lists. One missing extension is enough to separate a browser from Chrome. On 25 August 2026 our Chromium 151 build sent 16 extensions where branded Chrome 151 with its variations seed sent 17, the extra one being 0x12e0 from a Chrome field trial, in 3 runs without spread.
1, TLS extension, beside a row of tokens, the last one highlighted

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.

ConnectionExtension order as sent (hex, GREASE included)
1fafa, 002d, fe0d, 001b, 002b, 0005, 0023, ff01, 0010, 44cd, 0000, 0012, 000a, 000b, 0017, 0033, 000d, 7a7a
23a3a, 000b, 0012, 44cd, 000d, 0017, 001b, 0010, 002b, ff01, 0023, 0000, fe0d, 0033, 000a, 002d, 0005, fafa
3caca, 0023, 000d, fe0d, 001b, 002d, 002b, 0033, 000a, ff01, 0000, 44cd, 0005, 000b, 0010, 0012, 0017, 5a5a
Source: Scalebrowser header probe, one running Chrome 150 process, 3 consecutive TLS connections, August 2026

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:

  1. t says TLS over TCP; QUIC would be q.
  2. 13 is the TLS version, here TLS 1.3.
  3. d means the client sent a server name (SNI); a connection to a bare IP address shows i, which no real website ever sees.
  4. 15 is the number of cipher suites, GREASE removed.
  5. 16 is the number of extensions, GREASE removed.
  6. h2 is the first and last character of the first ALPN value, here HTTP/2.
  7. 8daaf6152771 is the SHA-256 of the sorted cipher list, truncated to 12 characters.
  8. 806a8c22fdea is 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.

On 25 August 2026 one extension from a Chrome field trial split our build's JA4 from branded Chrome 151, 16 extensions against 17, before any script ran.
Browser, dateJA4, first and last partSends 0x12e0
our engine 150, 12 Augt13d1516h2 … 806a8c22fdeano
real Chrome 150, 12 Augt13d1516h2 … 806a8c22fdeano
our engine 151.0.7922.99, 25 Augt13d1516h2 … 806a8c22fdeano
branded Chrome 151 with seed, 25 Augt13d1517h2 … c4845d6bf6c5yes
Source: Scalebrowser header probe, same Windows host, 3 runs per browser, 12 and 25 August 2026

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 our 151 build sent a handshake that matched Chrome 150 while its user agent said 151, and a server that knows the Chrome population can see such a contradiction before the page loads. Our reading of the seed put the study at about 100 percent of Chrome 151 users. That number was too high, as the 154 measurement below shows.

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 did the 154 measurement show?

On 26 September 2026 we measured our 154 engine against branded Chrome 154 on the same host, once with the variations seed applied and once with a fresh profile. Real Chrome sent 17 extensions and no 0x12e0 both times, and our build sent the same JA4, character for character.

BrowserJA4ExtensionsSends 0x12e0
branded Chrome 154.0.8037.57 with seedt13d1517h2_8daaf6152771_cb7bf5808d9917no
the same Chrome, fresh profileidentical17no
our engine 154.0.8037.58t13d1517h2_8daaf6152771_cb7bf5808d9917no
Source: Scalebrowser header probe, same Windows host, 26 September 2026

The seed explains the result. PqcBandwidthExperiment is tied to a layer: Chrome gives every client one random slot out of 100, and the study runs only in the member that holds 6 of those slots. So about 6 percent of Chrome 154 users send the extension. Our seed reader did not model layers and reported 100 percent. It now applies Chromium's full study filter, and for the seed of this Chrome it predicts all 163 studies without a layer that the browser itself lists in chrome://version, with no wrong entry either way. Switching the feature on, as the first number suggested, would have put every one of our browsers into the 6 percent.

One limit stays. Our pre-JavaScript gate compares the build against Chrome for Testing, which runs without a seed, so a field trial is only visible in a comparison with branded Chrome like the one above.

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. A seed tells you which studies a Chrome could run; the browser's own chrome://version tells you which it does. 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.

Real browser profiles for your AI agent, on your PC.

Each profile keeps its own identity, logins and proxy. Your agent drives it over MCP and signs in without seeing a password.

Download free for Windows

Free plan with 3 profiles · No credit card · Runs on your PC

Written by

Davide Valli

Founder, Scalebrowser

Builds Scalebrowser, browser automation for the agentic era that runs on your own machine. Measures every change a web page could observe against a real browser before it ships, and writes up the ones that turned out wrong.

All articles by Davide Valli

More in Detection

All 19 articles in Detection