---
title: "JA4 vs JA3: Why TLS Fingerprinting Migrated"
description: "JA3 to JA4 migration: how Chrome's GREASE-driven extension reordering broke JA3 and what FoxIO changed in the 2023 redesign."
kind: article
maturity: budding
confidence: medium
origin: ai-drafted
author: "Agent"
directedBy: "krow"
tags: [tls, ja4, ja3, fingerprinting, bot-detection]
published: 2026-05-20
modified: 2026-06-25
wordCount: 2196
readingTime: 10
prerequisites: [bot-detection-2026]
related: [bot-detection-2026, ja4-plus-fingerprint-suite, ja4t-tcp-fingerprinting, ja4-fingerprint-t13d1516h2, tls-impersonation-library-comparison]
url: https://krowdev.com/article/ja4-vs-ja3/
---
## Agent Context

- Canonical: https://krowdev.com/article/ja4-vs-ja3/
- Markdown: https://krowdev.com/article/ja4-vs-ja3.md
- Full corpus: https://krowdev.com/llms-full.txt
- Kind: article
- Maturity: budding
- Confidence: medium
- Origin: ai-drafted
- Author: Agent
- Directed by: krow
- Published: 2026-05-20
- Modified: 2026-06-25
- Words: 2196 (10 min read)
- Tags: tls, ja4, ja3, fingerprinting, bot-detection
- Prerequisites: bot-detection-2026
- Related: bot-detection-2026, ja4-plus-fingerprint-suite, ja4t-tcp-fingerprinting, ja4-fingerprint-t13d1516h2, tls-impersonation-library-comparison
- Content map:
  - h2: JA3: the 2017 design
  - h3: Why JA3 was good enough for a while
  - h2: What broke JA3
  - h3: GREASE
  - h3: Extension order permutation
  - h3: Three more things JA3 never modeled
  - h2: JA4: the FoxIO redesign
  - h3: Decomposing t13d1516h2
  - h2: The JA4+ suite
  - h2: Why the vendors moved
  - h3: What does not change
  - h2: What to take away
  - h2: Sources
- Crawl policy: same canonical content is exposed through HTML, Markdown, and llms-full; no crawler-specific content gate.

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](/article/bot-detection-2026/) is the prerequisite. If you need the non-TLS siblings, the [JA4+ fingerprint suite](/article/ja4-plus-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](/snippet/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):

```text
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](/article/bot-detection-2026/#ja3-the-original-now-largely-obsolete). 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+](https://github.com/FoxIO-LLC/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:

| Segment | Value | Meaning |
|---|---|---|
| Protocol | `t` | TCP transport (`q` would mean QUIC) |
| TLS version | `13` | TLS 1.3 negotiated in the `supported_versions` extension (falls back to legacy `version` field otherwise) |
| SNI present | `d` | Domain SNI was sent (`i` means IP-direct, no SNI) |
| Cipher count | `15` | 15 cipher suites after GREASE removal and deduplication |
| Extension count | `16` | 16 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 ALPN | `h2` | First 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:

```text
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](/snippet/ja4-fingerprint-t13d1516h2/).

## The JA4+ suite

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

| Fingerprint | Layer | What it covers |
|---|---|---|
| **JA4** | TLS client | ClientHello: version, SNI, ciphers, extensions, ALPN, signature algorithms |
| **JA4S** | TLS server | ServerHello: version, selected cipher, extensions, selected ALPN |
| **JA4H** | HTTP | Method, version, cookie/referer presence, accept-language, header order |
| **JA4L** | Latency | Round-trip latency derived from TLS handshake timing — useful for distance/geography sanity checks |
| **JA4X** | X.509 | Certificate issuer, subject, extensions — for clustering certs themselves |
| **JA4T** | TCP | Window size, options, MSS, TTL — Layer 0 of the stack |
| **JA4SSH** | SSH | SSH 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](https://blog.cloudflare.com/ja4-signals/) (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](/article/bot-detection-2026/#layer-2-http2-settings)) will finish the job.

## Sources

- [FoxIO JA4+ specification](https://github.com/FoxIO-LLC/ja4) — canonical JA4 format and reference implementations, including JA4S/JA4H/JA4L/JA4X/JA4T/JA4SSH.
- [Salesforce JA3 announcement](https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/) — original 2017 writeup by Althouse/Atkinson/Atkins.
- [RFC 8701: GREASE](https://datatracker.ietf.org/doc/html/rfc8701) — the anti-ossification mechanism that initially destabilized JA3.
- [Chrome TLS extension permutation](https://chromestatus.com/feature/5124606246518784) — the Chrome 110 change that finished the job.
- [Cloudflare JA3/JA4 fingerprint signals](https://developers.cloudflare.com/bots/additional-configurations/ja3-ja4-fingerprint/) — how a major CDN exposes both fingerprints to customer rules.
- [How Websites Detect Bots in 2026](/article/bot-detection-2026/) — the broader layered detection stack JA4 sits inside.
- [JA4 fingerprint t13d1516h2](/snippet/ja4-fingerprint-t13d1516h2/) — short reference for the most common modern Chromium JA4 prefix.