← Back to Content

JA4 Fingerprinting: What It Is, and Why One Chrome Browser Shows Up as 16

A JA4 fingerprint is only as good as whoever computes it. How GREASE turns one Chrome browser into 16 JA4s, and how to check your edge.

Robin
ja4 fingerprinttls fingerprintingja3 fingerprintbrowser fingerprinting
JA4 Fingerprinting: What It Is, and Why One Chrome Browser Shows Up as 16

Log the JA4 fingerprint your Content Delivery Network (CDN) reports for a single, unchanged Chrome browser over a day of fresh connections, and you may find 16 different values. A JA4 fingerprint is supposed to be the stable TLS fingerprint that fixed what broke JA3, so that shouldn't happen. Chrome is behaving correctly and JA4 itself is sound. The problem is an implementation that doesn't follow one of JA4's rules. Here's how that happens, how to check what your own edge computes, and what to do about it.

What a JA4 Fingerprint Is

A JA4 fingerprint is a short, readable summary of the TLS Client Hello: the first message a client sends when it opens an encrypted connection. It records the protocol, TLS version, cipher suites, extensions and signature algorithms the client offers, hashed into three segments. It is computed server-side, before any JavaScript runs.

Transport Layer Security (TLS) clients are built on a handful of libraries, and each one opens a connection in a characteristic way. That is what a JA4 captures: the TLS stack, not the person using it. It is a different layer from browser fingerprinting, which reads attributes of the browser environment from inside the page.

This is the example Chrome fingerprint from the JA4 specification, broken into its parts:

# A JA4 fingerprint, segment by segment (example from the FoxIO JA4 spec)
t13d1516h2_8daaf6152771_e5627efa2ab1

t      transport: t = TLS over TCP, q = QUIC, d = DTLS
13     highest TLS version offered (1.3)
d      SNI present: d = a domain name was sent, i = connecting to a bare IP
15     number of cipher suites offered
16     number of extensions offered
h2     first and last character of the first ALPN value ("h2" = HTTP/2)

8daaf6152771   cipher suites, sorted, SHA-256, first 12 hex characters
e5627efa2ab1   extensions sorted (SNI and ALPN removed) + signature
               algorithms in wire order, SHA-256, first 12 hex characters

The first segment is plain text you can read at a glance. The second and third are hashes, but they are reproducible: sort the values the way the spec says, hash them, and you get the same 12 characters every time. The spec also defines a raw form, JA4_r, which keeps the unhashed lists. Keep that in mind, because it matters later.

JA3 vs JA4: What Changed, and Why It Had To

The ja3 fingerprint came first. Salesforce published JA3 in 2017: take five fields from the Client Hello (TLS version, cipher suites, extensions, elliptic curves and point formats) in the order the client sent them, join them into a string and hash it with MD5. For several years that was the standard way to recognise a TLS client.

Two things wore it down. The first was the hash itself. An MD5 tells you two JA3s are different, but not how: you can compare them, never read them. The second was fatal. In 2023 Chrome began randomising the order of its TLS extensions on every connection, so the same browser produced a different JA3 each time it connected. A fingerprint that changes per connection can't recognise anything.

JA4, released by FoxIO in 2023, answers both problems:

  • It sorts before hashing. Cipher suites and extensions are sorted by value, so extension-order randomisation no longer changes the result.

  • It is segmented and partly readable. You can see the protocol, version and counts without decoding anything, and compare the cipher and extension hashes separately.

  • It ignores GREASE. The spec tells implementations to "ignore GREASE values anywhere it sees them." GREASE is explained in the next sections, and it's the root of the 16-fingerprint problem.

JA4 is a genuine improvement on JA3. Nothing in this article is a case against it. The case is about implementations that don't apply all of its rules.

The JA4+ Suite and Its Licensing Split

JA4 is one member of a family. FoxIO also defines JA4S (the server's response), JA4H (HTTP requests), JA4T (TCP), JA4X (certificates), JA4SSH and others, collectively called JA4+.

The licensing differs across the family, and that matters before you build on it. According to FoxIO's License FAQ, JA4 itself is BSD 3-Clause, the same as JA3, and FoxIO says it won't pursue patents on it. The rest of JA4+ (JA4S, JA4L, JA4LS, JA4H, JA4X, JA4SSH, JA4T, JA4TS, JA4TScan, JA4Scan, JA4D and JA4D6) is under the FoxIO License 1.1 and is patent pending.

That licence allows internal business use and academic use. It doesn't allow monetisation: selling JA4+ as part of a product, or using it to deliver value to paying customers, needs an OEM licence from FoxIO. Using the JA4 TLS client fingerprint is unrestricted.

One Chrome Browser, 16 JA4 Fingerprints

What GREASE is

GREASE stands for Generate Random Extensions And Sustain Extensibility, defined in RFC 8701 in 2020. The problem it solves is ossification: if every client always sends the same handshake shape, middleboxes start assuming that shape, and the protocol can no longer change without breaking them. So clients deliberately send reserved, meaningless values that a well-behaved server ignores.

There are 16 reserved GREASE values: 0x0A0A, 0x1A1A, 0x2A2A and so on up to 0xFAFA. RFC 8701 allows them in cipher suites, extensions, supported groups, versions, Application-Layer Protocol Negotiation (ALPN) values and signature algorithms. Chrome picks a value at random for each connection.

That randomness is why the JA4 spec says to ignore GREASE wherever it appears. A GREASE value carries no information about the client, and keeping it in the hash would make one client look like up to 16.

What we saw in production

We found this in our own traffic. ThumbmarkJS runs on our AWS multi-region architecture, and we use the JA4 fingerprint that AWS CloudFront, Amazon's CDN, computes for each request. To be clear, because the names are close: this concerns AWS CloudFront, not Cloudflare. We haven't tested Cloudflare's JA4 implementation and make no claim about it either way.

The fingerprints CloudFront reported for one unchanged Chrome browser didn't hold still. The table below shows the 16 JA4 values one Chrome browser can produce behind CloudFront, one for each GREASE value Chrome might pick for the signature-algorithm list. The first two segments never change; the third does, every time.

GREASE value in signature algorithms

JA4 reported by CloudFront

0a0a

t13d1516h2_8daaf6152771_c95be0ae8015

1a1a

t13d1516h2_8daaf6152771_9a116fd1b484

2a2a

t13d1516h2_8daaf6152771_7850381cb412

3a3a

t13d1516h2_8daaf6152771_916fe344f6aa

4a4a

t13d1516h2_8daaf6152771_64075eac4a92

5a5a

t13d1516h2_8daaf6152771_4c57c08d3a5a

6a6a

t13d1516h2_8daaf6152771_3557bd83d7c8

7a7a

t13d1516h2_8daaf6152771_0f91466e0738

8a8a

t13d1516h2_8daaf6152771_9654de2595cf

9a9a

t13d1516h2_8daaf6152771_9f880d3ff939

aaaa

t13d1516h2_8daaf6152771_c5fb635eb5d4

baba

t13d1516h2_8daaf6152771_7965d1fc1fcf

caca

t13d1516h2_8daaf6152771_816bbdc96cb0

dada

t13d1516h2_8daaf6152771_99f513a60efa

eaea

t13d1516h2_8daaf6152771_2af83cb4fe89

fafa

t13d1516h2_8daaf6152771_005bd6d5397d

The stable parts tell you what CloudFront gets right. Chrome puts GREASE in its cipher list, its extension list and its version field, and CloudFront strips all of it: the counts in the first segment and the cipher hash in the second are identical in every row.

The varying part tells you what it misses. Signature algorithms feed only the third segment, and CloudFront keeps Chrome's GREASE value in that list. Sixteen possible values in, sixteen different hashes out.

Why it started in 2026

RFC 8701 has allowed GREASE in signature algorithms since 2020, but browsers didn't use it there. That changed in June 2026, when BoringSSL, the TLS library Chrome is built on, added a GREASE value to the signature-algorithm list, initially behind an opt-in switch. In October 2026 it became part of BoringSSL's standard GREASE behaviour.

That timing matters for how to read the result. Before mid-2026, an implementation that stripped GREASE from ciphers, extensions and versions but not from signature algorithms produced correct output for every mainstream browser, because there was nothing in the signature-algorithm list to strip. This looks like a latent gap that a browser change exposed, not a careless implementation.

It's also fixable, and the fix is known. AWS's own open-source TLS library, s2n-tls, has skipped GREASE in signature algorithms since its JA4 support landed in 2024. We don't know whether CloudFront uses s2n-tls to compute JA4, so this isn't a claim about CloudFront's code. It does show the spec-compliant behaviour is a few lines, not a research problem.

The general lesson

A JA4 is only as good as whoever computed it. "Supports JA4" in a CDN or web application firewall feature list tells you a field exists, not that its value follows the spec, and the gap can open after you've integrated, when a browser changes underneath you. If you didn't compute the JA4 yourself, treat it as a claim to verify, not a measurement.

How to Check What Your Edge Actually Computes

Detect it

The test is stability. Take one browser you control, open a series of fresh connections to an endpoint behind your edge, and record the JA4 the edge reports for each. A spec-compliant JA4 from an unchanged browser should come out the same every time.

On CloudFront, add the CloudFront-Viewer-JA4-Fingerprint header to an origin request policy and log it at your origin. Add CloudFront-Viewer-TLS too: its last field says whether each connection was a fullHandshake, a sessionResumed or a connectionReused. Anyone on CloudFront can reproduce the result above this way.

That second header matters, because not all variance is a bug. Two kinds look similar in a log:

  • Resumption variance is by design. When a client resumes an earlier session, its Client Hello changes: the session-ticket extension is swapped for a pre-shared-key extension, and early data or padding may be added. The extension count in the first segment changes (for example t13d1515h2 becomes t13d1516h2), and so does the third segment. That's the spec working as intended, so compare full handshakes only.

  • GREASE contamination leaves the first two segments identical and changes only the third, cycling through up to 16 values for the same full handshake.

If your full-handshake JA4s for one Chrome browser share their first 22 characters and differ only in the last 12, you are looking at the same problem as the table above.

Deal with it

A JA4 is a hash, so a contaminated one can't be cleaned after the fact: there's no way to remove a GREASE value from a truncated SHA-256. That leaves three practical options.

  • Key on the first two segments only. The t13d1516h2_8daaf6152771 prefix is stable and available today. You lose the extension and signature-algorithm information, which makes the fingerprint less specific: many Chrome versions share that prefix. For recognising a TLS stack rather than a version of it, that's often enough.

  • Treat the variants as one. Group JA4s that differ only in the third segment into a single equivalence class. Doing this exactly needs the raw handshake data (the JA4_r form), so you can see that the only difference is a GREASE value. A list of known variants, like the table above, is the practical version of a JA4 lookup for clients you care about.

  • Compute JA4 yourself. If you terminate TLS somewhere you control, such as a load balancer or reverse proxy, compute the fingerprint there with a spec-compliant implementation. FoxIO's repository includes reference implementations, and JA4 itself is BSD-licensed.

Which one fits depends on what you key on JA4 for. Allow-listing known automation or blocking a known-bad client works with the prefix. Anything that needs to tell one Chrome build from another needs the raw data or your own computation.

Pairing a JA4 with the Browser's Own Signals

Every use of a JA4 assumes the JA4 is correct. A naive "does this look like Chrome?" rule, run on CloudFront's values, would flag real Chrome users, because real Chrome now arrives as 16 different fingerprints. Correctness is the precondition. Pairing is what you build on top of it.

The useful property of a JA4 is that it's collected at a different layer from everything the page can see. The browser reports a user agent and a set of client-side signals like canvas and audio; the TLS layer independently reports which stack opened the connection. When those disagree, for example a Client Hello that doesn't look like Chrome's arriving with a Chrome user agent and a Chrome-shaped client fingerprint, that's a strong sign of automation. How detection systems weigh that kind of mismatch is covered in our article on bot detection.

The more common case in practice runs the other way. Off-the-shelf HTTP libraries can make a script's Client Hello match a real browser's, and getting past TLS fingerprint checks this way is routine in automated traffic. So a Chrome-shaped JA4 tells you which TLS stack a client claims to be, not what it is.

That doesn't make the JA4 useless; it changes what you check. A script that forges the handshake usually runs no JavaScript, so the browser-side half of the picture is missing entirely. A perfect Chrome handshake with no browser signals behind it is itself a mismatch, and one that TLS-only checks can't see.

Conclusion

JA4 fixed what extension randomisation did to JA3, and its rule for GREASE is clear: ignore it everywhere. But a JA4 is computed by someone, and when that someone is your CDN, a change in the browser can make its output wrong without anything changing on your side. Verify what your edge reports against one browser you control before you key decisions on it, and pair it with signals from a different layer. The JA4 specification is the reference for what a correct value looks like, and the ThumbmarkJS open-source library covers the browser-side half if you want to compare the two.

Frequently Asked Questions

What is a JA4 fingerprint?

A JA4 fingerprint is a three-part summary of a TLS Client Hello: a readable prefix with the protocol, TLS version, cipher and extension counts and ALPN, followed by a hash of the cipher suites and a hash of the extensions and signature algorithms. It identifies the TLS stack a client uses, not the person using it.

Is JA4 better than JA3?

Yes. JA3 hashes fields in the order the client sends them, so Chrome's extension-order randomisation, introduced in 2023, gives one browser a new JA3 on every connection. JA4 sorts before hashing, ignores GREASE values and keeps part of the fingerprint readable. Its stability does depend on implementations applying those rules correctly.

Can a JA4 fingerprint identify a specific user?

No. Everyone running the same version of the same browser on the same TLS library produces the same JA4. It identifies a client implementation, such as a Chrome build, a Python HTTP library or a piece of malware, which is useful for spotting automation but not for recognising an individual visitor.

Does TLS extension randomisation break JA4 the way it broke JA3?

No. JA4 sorts cipher suites and extensions before hashing, so their order doesn't affect the result. What can break a JA4 is GREASE values that an implementation fails to remove. Since Chrome started adding GREASE to its signature-algorithm list in 2026, an implementation that misses it there produces up to 16 different JA4s for one browser.

Can a JA4 fingerprint be spoofed?

Yes. Some HTTP libraries can reproduce a real browser's Client Hello, so a script can present Chrome's JA4. That's why a JA4 works best as one signal among several: a browser-shaped handshake with no browser-side signals behind it is a mismatch that a TLS-only check can't detect.