Agent context packet

Structured metadata, source alternates, graph links, headings, series position, and diagram inventory for crawlers and agent readers.

JA4 vs JA3: Why TLS Fingerprinting Migrated

JA3 to JA4 migration: how Chrome's GREASE-driven extension reordering broke JA3 and what FoxIO changed in the 2023 redesign.

/ directed by / / 10 min read
On this page

JA3 was the dominant TLS client fingerprint from 2017 through roughly 2023. By 2026, every major detection vendor — Cloudflare, AWS WAF, Akamai, VirusTotal — has either replaced JA3 with JA4 or runs both in parallel and weights JA4 higher. This article is about why that migration happened and what the redesign actually changed.

If you need a recap of where TLS fingerprinting sits in the broader detection stack, How Websites Detect Bots in 2026 is the prerequisite. If you need the non-TLS siblings, the JA4+ fingerprint suite maps JA4S, JA4H, JA4X, JA4L, JA4SSH, and JA4T by layer. If you only want to know what a specific prefix decodes to, JA4 fingerprint t13d1516h2 is the short version.

JA3: the 2017 design

JA3 was published by John Althouse, Jeff Atkinson, and Josh Atkins at Salesforce in 2017 (the name is the three Js). It was the first widely adopted TLS client fingerprint, and the idea was deliberately simple: pull a handful of fields out of the ClientHello, concatenate them in observation order, and MD5 the result.

The fields, in order:

  1. SSL/TLS version (the legacy version field, decimal)
  2. Cipher suites (decimal, dash-separated, in the order the client sent them)
  3. Extensions (decimal, dash-separated, in the order the client sent them)
  4. Elliptic curves (the supported_groups extension contents)
  5. EC point formats (the ec_point_formats extension contents)

Concatenate with commas, MD5 the whole string, and that 32-hex-character output is the JA3.

A toy example of the pre-hash string (illustrative, not a real capture):

771,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

771 is TLS 1.2 in the legacy version field. The hyphen-delimited cipher list and extension list preserve observation order. That order dependency is the single design choice that everything else in this article hinges on.

JA3S was the matching server-side fingerprint over the ServerHello: TLS version, single selected cipher, extension list. Useful for malware-family clustering because C2 servers tend to have very specific ServerHello shapes, less useful for browser identification.

Why JA3 was good enough for a while

In 2017, ClientHello shape was a strong identifier. Chrome, Firefox, and Safari each used different TLS libraries — BoringSSL, NSS, SecureTransport — and each library produced a deterministic ordering. Two captures of the same Chrome version from the same OS produced the same JA3. Python requests produced its own stable JA3 that matched no browser. The hash space was clean enough that threat-intel feeds could just publish lists of “known bad” JA3 values and call it a day.

Then Chrome shipped GREASE, then it shipped extension permutation, and the whole premise collapsed.

What broke JA3

GREASE

GREASE (Generate Random Extensions And Sustain Extensibility, RFC 8701) was Google’s answer to protocol ossification. Servers and middleboxes had a long history of rejecting any TLS field value they did not recognize, which made it impossible to introduce new cipher suites or extensions without breaking deployments. GREASE makes Chrome insert randomly-chosen reserved values into cipher lists, extension lists, supported groups, and ALPN, so that any middlebox brittle enough to choke on an unknown value breaks early and gets fixed.

The GREASE values are drawn from a fixed set (0x0A0A, 0x1A1A, 0x2A2A, … 0xFAFA) and Chrome picks one at startup. That choice ends up inside the JA3 input string as a regular decimal cipher or extension number. Different Chrome instances pick different GREASE values, so they produce different JA3 hashes — for the exact same Chrome binary on the exact same OS.

You could strip GREASE before hashing — and several JA3 implementations eventually did — but that was a patch on top of a spec that did not mandate it. Feeds and detection rules disagreed on whether the GREASE-stripped hash or the raw hash was canonical, and the same client could appear under two different fingerprints depending on which implementation observed it.

Extension order permutation

The bigger problem arrived with Chrome 110 in January 2023. Chrome started randomly permuting the order of most TLS extensions on every ClientHello. The motivation was identical to GREASE — prevent ossification by ensuring no server could depend on a specific extension order — and the side effect on JA3 was total.

JA3 hashes the extension list in observation order. Permute the order, change the hash. Chrome 110+ produces effectively a different JA3 on every connection. Empirically, the same Chrome major-version target captured at different times yields different JA3 hashes — see the per-version table in the bot-detection overview. The hashes are not stable within a Chrome major version, let alone across versions.

Firefox followed with its own extension shuffling, and any TLS library wrapping BoringSSL inherited the behavior automatically.

The detection consequence: JA3 stopped being useful for identifying browsers and remained useful only for identifying non-browsers. Python requests, Go’s net/http, default curl — none of them randomize, so they still produce stable JA3 hashes that match no real browser. That residual signal is why JA3 has not been fully retired yet, but it does not justify keeping it as the primary fingerprint.

Three more things JA3 never modeled

Even before GREASE, JA3 had structural gaps:

  • No SNI awareness. The Server Name Indication extension is one of the highest-signal fields in the ClientHello — its presence, absence, length, and contents tell you whether the client is doing IP-direct connections, DNS-over-HTTPS bootstraps, or normal browser navigation. JA3 only knows that extension 0 was present in the list; it does not encode whether SNI was actually populated.
  • No ALPN awareness. Whether the client advertised h2, http/1.1, or both is a strong browser-vs-script signal. JA3 collapses ALPN to “extension 16 was in the list.”
  • MD5. Not a security problem (this is not a security hash), but a tooling problem: MD5 fingerprints are not directly human-readable. You cannot glance at e7d705a3286e19ea42f587b344ee6865 and know what kind of client it represents. Every lookup goes through a database.

These were tolerable limits in 2017. By 2023 they were the spec, and JA4 was designed against them.

JA4: the FoxIO redesign

JA4+ was published by FoxIO in 2023, with John Althouse — one of the original JA3 authors — leading the redesign. It is not an incremental tweak; it is a new format that takes the lessons of six years of evasion and bakes them in.

The JA4 TLS client fingerprint has three parts: a_b_c.

  • Part A is human-readable. You can read it directly off a log line.
  • Part B is the first 12 hex characters of the SHA256 of the sorted, GREASE-stripped cipher suite list.
  • Part C is the first 12 hex characters of the SHA256 of the sorted, GREASE-stripped extension list, with signature algorithms appended in their original observed order.

Sorting before hashing is the single most important change. Chrome can permute extensions on every connection — the sorted hash is stable. GREASE values are stripped explicitly in the spec, so there is no implementation disagreement.

Decomposing t13d1516h2

Part A is a fixed-width readable string. Take the most common modern Chromium prefix:

SegmentValueMeaning
ProtocoltTCP transport (q would mean QUIC)
TLS version13TLS 1.3 negotiated in the supported_versions extension (falls back to legacy version field otherwise)
SNI presentdDomain SNI was sent (i means IP-direct, no SNI)
Cipher count1515 cipher suites after GREASE removal and deduplication
Extension count1616 extensions after GREASE removal. SNI (type 0) and ALPN (type 16) are counted here — they’re only excluded from the JA4_c hash, not the count
First ALPNh2First ALPN value advertised was HTTP/2 (would be h1 for HTTP/1.1, 00 for none)

Two design decisions are worth pulling out. First, SNI and ALPN are pulled out of the extension count and surfaced as their own segments — exactly the gap JA3 had. Second, t13 reflects the negotiated TLS version from supported_versions, not the legacy ClientHello version field, which has been pinned at TLS 1.2 (0x0303) by every modern client for compatibility reasons.

A full Chrome-family JA4 looks like:

t13d1516h2_8daaf6152771_d8a2da3f94cd

The 8daaf6152771 is the cipher-list hash; the d8a2da3f94cd is the extension-list-plus-signature-algorithms hash. The middle hash is stable across Chrome’s extension permutation because the inputs are sorted before hashing. The tail hash typically only changes when Chrome modifies its signature_algorithms list, which happens every few major versions.

For deeper version-to-fingerprint mappings, see the t13d1516h2 reference.

The JA4+ suite

JA4 is not a single fingerprint, it is a family designed to be composable across the protocol stack. The current set:

FingerprintLayerWhat it covers
JA4TLS clientClientHello: version, SNI, ciphers, extensions, ALPN, signature algorithms
JA4STLS serverServerHello: version, selected cipher, extensions, selected ALPN
JA4HHTTPMethod, version, cookie/referer presence, accept-language, header order
JA4LLatencyRound-trip latency derived from TLS handshake timing — useful for distance/geography sanity checks
JA4XX.509Certificate issuer, subject, extensions — for clustering certs themselves
JA4TTCPWindow size, options, MSS, TTL — Layer 0 of the stack
JA4SSHSSHSSH client/server fingerprints, parallel design to JA4/JA4S

The composability matters. A detection rule can match on JA4 + JA4H + JA4T simultaneously, and a request that fakes one layer but not the others fails cross-layer consistency. Cloudflare exposes JA3 and JA4 in their bot signals; their newer rules are written against JA4 because the hash is stable enough to actually rule on.

Why the vendors moved

Migration was not free. Each detection vendor had to:

  • Re-instrument capture: JA4 needs supported_versions, ALPN values, and signature algorithms surfaced as separate fields, not just rolled into one extension list.
  • Re-train models: anything that consumed JA3 as a feature had to be retrained on JA4 with new cardinality. Cloudflare reports analyzing over 15 million unique JA4 fingerprints daily (across 500M+ user agents); the JA3 distribution had been polluted by per-connection randomization noise.
  • Maintain both: existing customer rules referenced JA3, so vendors run JA4 alongside JA3 rather than ripping JA3 out. Cloudflare, AWS WAF, and Akamai all expose both fields in 2026.

The migration calculus was straightforward: JA3 had degraded from a high-precision signal to a coarse “is this a browser or a script” classifier. JA4 restored per-browser-family precision and added the SNI/ALPN/signature-algorithm signals that JA3 never had. For a detection vendor whose value proposition is precision, keeping JA3 as primary was untenable.

What does not change

A few things to be honest about:

  • JA4 does not defeat impersonation libraries. A tool like curl_cffi that replays a real Chrome ClientHello byte-for-byte produces the real Chrome JA4. The fingerprint is honest about what the network saw; if what the network saw is indistinguishable from Chrome, the fingerprint matches Chrome. JA4 raises the bar — you have to replay the full ClientHello, not just match a hash of order-sensitive fields — but it does not close the gap on its own.
  • JA4 is one layer of a stack. A request can have a perfect JA4 and fail on HTTP/2 SETTINGS, header order, or sec-ch-ua coherence. Cross-layer consistency is the actual detection surface; JA4 is one column in that table.
  • JA3 is not zero-value. It still catches naive scripts cheaply. As a low-cost first-pass filter against requests, httpx, default curl, and Go’s net/http, JA3 is fine. The mistake is treating it as a browser-version identifier.

What to take away

JA3’s design assumed TLS field order was a stable property of a client. GREASE and Chrome 110’s extension permutation made that assumption false, and the failure was structural — you cannot patch a hash that depends on order into being order-invariant without changing the spec. JA4 changed the spec: sort before hashing, strip GREASE explicitly, surface SNI/ALPN/signature-algorithms as first-class fields, and emit a human-readable prefix so analysts can grep logs without a lookup table.

If you are writing new detection rules, write them against JA4. If you are reading old detection rules, expect JA3 to be present but downweighted. If you are on the other side of the wire and your tooling produces a JA3 of e7d705a3286e19ea42f587b344ee6865 — the canonical Tor-client hash that has sat in threat feeds for years (Python requests has its own equally-published value) — JA4 is going to flag you just as cleanly, and the rest of the stack (HTTP/2 SETTINGS, header order) will finish the job.

Sources

Diagram

Drag to pan · scroll or pinch to zoom · Esc to close