JA4 and JA4T Fingerprints, Field by Field
A JA4 fingerprint is a string computed from a client's TLS ClientHello: the transport, the TLS version, whether SNI was present, how many ciphers and extensions were offered, the first ALPN value, then two truncated SHA-256 hashes over the sorted cipher list and the sorted extension list. JA4T is a separate fingerprint computed one layer down, from the fields of the TCP SYN packet.
Both belong to JA4+, created by John Althouse at FoxIO and launched in September 2023. The two strings look interchangeable in a log. They are not. They describe different layers, they take different collection, and only one of them has a written specification.
What is a JA4 fingerprint?
JA4 records the choices a client's TLS library made when it opened the connection. The ClientHello is cleartext and arrives before any encrypted data, so whatever terminates or observes the handshake can read the fields it needs.
The output is three sections joined by underscores, called a, b and c. FoxIO's specification works one example end to end:
t13d1516h2_8daaf6152771_e5627efa2ab1Section a is readable on sight. Sections b and c are hashes.
What does each field in a JA4 fingerprint mean?
Section a packs six fields into ten characters with no separators.
| Field | Value in the example | Meaning |
|---|---|---|
| Transport | t | TLS over TCP. q is QUIC, d is DTLS. |
| TLS version | 13 | TLS 1.3. |
| SNI | d | SNI was present, so the destination is a domain. i means no SNI, so the destination is an IP address. |
| Cipher count | 15 | Fifteen cipher suites offered. |
| Extension count | 16 | Sixteen extensions offered. |
| ALPN | h2 | First and last ASCII alphanumeric characters of the first ALPN value. 00 if there is no ALPN extension, no values, or an empty first value. |
Three details in that table are where implementations disagree with each other.
The TLS version is not the handshake version at the top of the packet, which is ignored. It comes from the highest value in the supported_versions extension (0x002b) when that is present, and from the legacy Protocol Version field when it is not. The codes run 13, 12, 11 and 10 for TLS 1.3 down to TLS 1.0, s3 and s2 for SSL 3.0 and SSL 2.0, d1, d2 and d3 for DTLS 1.0, 1.2 and 1.3, and 00 for anything unrecognized.
The counts exclude GREASE but include values that are not really ciphers: SCSV (0x00FF, 0x5600) and the experimental and reserved range 0xFE00 to 0xFEFF all count. GREASE is ignored everywhere it appears, in every field: "The program needs to ignore GREASE values anywhere it sees them."
And the extension count is the count on the wire, not the number of extensions that reach the hash. Those are different numbers, as section c shows.
Section b, the cipher hash
The cipher list, sorted into hex order, comma delimited, lowercase, hashed with SHA-256, truncated to the first 12 characters. In the worked example the offered ciphers sort to:
002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9That hashes to 8daaf6152771. An empty cipher list is not hashed at all, it is written as the literal 000000000000, and all SHA-256 output in JA4 is lowercase.
Section c, the extension and signature-algorithm hash
The extension list sorted by hex value, with two removed: SNI (0x0000) and ALPN (0x0010). Both are dropped because section a already carries them, which is what keeps section c stable for one application across different destinations and different negotiated ALPNs. The signature algorithm list is then appended after an underscore in wire order, unsorted, because order carries information there.
Sixteen extensions were counted in section a. Fourteen reach the hash input:
0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01_0403,0804,0401,0503,0805,0501,0806,0601That hashes to e5627efa2ab1. An empty extension list also yields 000000000000.
To debug a mismatch, use the raw output modes. -r emits the sorted lists unhashed, and -o emits them in their original order with GREASE excluded, renaming the field to ja4_o.
Why does JA4 exist when JA3 already did?
Because JA3 depended on ordering, and ordering is free to change. Attackers found that first. Then a browser did it too.
JA3 was created by John B. Althouse, Jeff Atkinson and Josh Atkins and open sourced by Salesforce in 2017, and its input included the cipher list in the order sent. Akamai's research team named the evasion that followed "cipher stunting" and traced its roots to early 2018: present a randomized cipher list in the ClientHello and the hash at the end randomizes with it. In August 2018, before Akamai saw any tampering, it observed 18,652 distinct fingerprints globally. Randomization appeared that September. By the end of October the tampering count had reached 255 million, and by the end of February 2019 the figure Akamai published was 1,355,334,179. It attributed the campaign, with what it called a high degree of certainty, to a Java-based tool.
Then Chrome did the same thing to extension order, for the opposite reason. Permuting extensions is an anti-ossification measure, keeping servers and middleboxes from depending on a fixed order, and the TLS 1.3 RFC permits any extension order except pre_shared_key, which must be sent last. The change was scheduled for Chrome 110 and shipped enabled by default in 108 and 109. Fastly watched the previously dominant Chrome JA3 value cd08e31494f9531f560d64c695473da9 fall off its network from January 20, 2023, put the number of possible permutations at nearly 15 factorial (about 10^12), and concluded that "we can expect each connection made with the TLS ClientHello extension permutation feature to practically have a unique JA3 fingerprint."
JA4 answers both by sorting before hashing. FoxIO on the ciphers: "in our research we've found that applications and libraries choose a unique cipher list more than unique ordering. This also reduces the effectiveness of 'cipher stunting'". Extensions are sorted for the same reason, in direct response to the Chromium change. What a client offers stays in the fingerprint. The order it offered in does not.
JA3 vs JA4
| JA3 | JA4 | |
|---|---|---|
| Output shape | One MD5 digest, 32 characters, no readable fields: cd08e31494f9531f560d64c695473da9 | Three underscore-separated sections, a readable prefix plus two 12-character truncated SHA-256 hashes: t13d1516h2_8daaf6152771_e5627efa2ab1 |
| Cipher order | Part of the hashed input, so randomizing it changes the fingerprint. This is what cipher stunting exploited. | Sorted by hex value before hashing, so order is irrelevant |
| Extension order | Part of the hashed input, so Chrome's per-connection permutation changes it every connection | Sorted before hashing. SNI and ALPN are excluded from the hash and represented in the readable prefix instead |
| Transport visible in the string | No field for it | First character: t for TLS over TCP, q for QUIC, d for DTLS |
| Origin | Created at Salesforce, open sourced 2017. Repository archived 2025-05-01. | Created by John Althouse at FoxIO. JA4+ launched September 2023. |
| Licence | BSD 3-Clause | JA4 itself BSD 3-Clause. Every other JA4+ method is under the FoxIO License 1.1. |
What does a JA4 fingerprint prove?
That a particular TLS stack, configured a particular way, opened the connection. Not which application by name, and not which device.
F5's technical write-up states the limit: "It is important to understand that a JA4 TLS fingerprint, or any TLS fingerprint for that matter, is NOT a fingerprint of an individual instance of a device or browser. Rather, it is a fingerprint of a TLS 'stack' or application. For example, all Chrome browsers of the same version and the same operating system will generate the same JA4 fingerprint." Its practical conclusion: do not act on a JA4 alone unless you are certain that all, or nearly all, requests carrying it are malicious.
The scale numbers agree. Cloudflare reports analyzing more than 15 million unique JA4 fingerprints daily, generated by over 500 million user agents and billions of IP addresses. That many-to-one behaviour is deliberate, since excluding SNI and ALPN and sorting the extensions is exactly what keeps one library's fingerprint stable across destinations.
The values also expire. Althouse is blunt about it: "JA4 fingerprints will change as application TLS libraries are updated, about once a year. Do not assume fingerprints will remain constant in an environment where applications are updated."
Two things a JA4 does not give you. There is no published collision rate or false-positive rate for JA4 as a detection signal. The available evidence is directional, F5's qualitative statement and Cloudflare's population figures, so a specific false-positive number quoted for JA4 anywhere is unsourced. And because the value comes only from handshake bytes, it says nothing about the network path: a client rotating through a residential proxy pool presents the same JA4 from every exit address, and that JA4 tells you nothing about those addresses. The IP-layer question takes a different class of data, the kind a VPN and proxy checker returns for a single address.
What is a JA4T fingerprint?
JA4T fingerprints the client's TCP SYN, using four fields joined by underscores: window size, the TCP option Kind list, maximum segment size, and window scale.
64240_2-1-3-1-1-4_1460_8There is no JA4T_ prefix inside the string, and the option list is not a run of zero-padded pairs. Documentation writes it as JA4T=64240_2-1-3-1-1-4_1460_8, where the JA4T= part is a label naming which fingerprint type follows. The stored value is the bare string. FoxIO's own mapping file holds exactly 64240_2-1-3-1-1-4_1460_8 in its ja4t column, hyphen separated, in unpadded decimal.
| Field | Value | What it is |
|---|---|---|
| Window size | 64240 | The receive window, the maximum data the client will accept before it needs an acknowledgment. Two bytes, and a required TCP header field. |
| Options | 2-1-3-1-1-4 | The TCP option Kind numbers, in decimal, in the order they appeared on the wire. Options are not required by TCP, but every modern operating system sends them. |
| MSS | 1460 | Maximum segment size, the largest payload the source will accept per packet. |
| Window scale | 8 | A multiplier on the window size. A window of 64240 with a scale of 8 is an effective window of 64240 x 2^8, or 16,445,440. |
Two of those fields are also TCP options, so they appear twice in the fingerprint: once as a Kind number inside the list, once as their own value. The list is also where the padding lives. A TCP options list must be divisible by four in total length, which is why the NOP option exists and why repeated entries turn up in real option lists.
MSS is the one field that reflects the path rather than the host. FoxIO gives 1460 as the most common initial value, derived from an Ethernet MTU of 1500, and reads anything lower as overhead. On the GreyNoise fingerprint 29200_2-4-8-1-3_1424_7: "an options list of 2-4-8-1-3 indicates a Unix-based operating system and an MSS of 1424 indicates that these connections have 36 bytes of additional network overhead."
What do the TCP option Kind numbers mean?
Each number in the options field is a Kind number from IANA's TCP option registry, the canonical assignment list for that header field. Six Kinds cover every option list FoxIO publishes.
| Kind | Registered name | Defining RFC | Option length |
|---|---|---|---|
0 | End of Option List | RFC 9293 | 1 byte, the kind field alone |
1 | No-Operation | RFC 9293 | 1 byte, the kind field alone |
2 | Maximum Segment Size | RFC 9293 | 4 bytes |
3 | Window Scale | RFC 7323 | 3 bytes |
4 | SACK Permitted | RFC 2018 | 2 bytes |
8 | Timestamps | RFC 7323 | 10 bytes |
The first two lengths are the exception rather than a rounding. The registry's own note: "Options 0 and 1 are exactly one octet which is their kind field. All other options have their one octet kind field, followed by a one octet length field, followed by length-2 octets of option data."
Kinds 0, 1 and 2 are what RFC 9293 calls the mandatory option set. RFC 9293, published in 2022, is the current TCP specification and the reference IANA cites for those three, having obsoleted RFC 793 for the base protocol. Window Scale and Timestamps are defined in RFC 7323, and SACK Permitted in RFC 2018.
With the legend, the common shapes decode on sight. 2-1-3-1-1-4, the Windows value above, reads Maximum Segment Size, No-Operation, Window Scale, No-Operation, No-Operation, SACK Permitted. 2-4-8-1-3, the GreyNoise fingerprint and the shape in three of FoxIO's five sample rows, reads Maximum Segment Size, SACK Permitted, Timestamps, No-Operation, Window Scale. A list ending in 0 or 0-0 ends in End of Option List bytes.
The lengths explain the No-Operation entries. The Windows list runs 4 plus 1 plus 3 plus 1 plus 1 plus 2 bytes, which is 12. The Unix list runs 4 plus 2 plus 10 plus 1 plus 3, which is 20. Both divide by four, and they divide because the single-byte No-Operation options are there to make them. Drop one and the total no longer divides.
That is also why the field is worth fingerprinting. TCP options are, in FoxIO's words, "not required but are used by every modern operating system", so the presence of any single Kind separates very little on its own. What separates is the combination and the sequence, and the requesting application chooses neither. The kernel's network stack builds the SYN below the socket API that userland code talks to, so the option order is a property of the host stack rather than of the request. FoxIO's framing: "Each operating system has different combinations of window size, options, and window scale."
Kind numbers above 8 do appear in real SYNs. The registry assigns Kind 34 to TCP Fast Open Cookie, defined in RFC 7413, and Kind 30 to Multipath TCP, defined in RFC 8684, both with variable length. A two-digit entry in an options list is not automatically a parsing error.
Can a JA4T identify the operating system?
Partly, and less precisely than most write-ups imply. No authoritative comprehensive table exists. What FoxIO publishes is a file it explicitly calls a sample for quick reference, with five client-OS rows.
| OS label in FoxIO's sample file | ja4t value |
|---|---|
| Windows 10 | 64240_2-1-3-1-1-4_1460_8 |
| WSL Ubuntu 22.04 | 64240_2-4-8-1-3_1460_7 |
| AWS Linux 2 | 62727_2-4-8-1-3_8961_7 |
| Ubuntu 22.04 | 65535_2-4-8-1-3_1460_8 |
| Mac OSX/iPhone | 65535_2-1-3-1-1-8-4-0-0_1460_6 |
Read those rows before building a rule on window size. 64240 covers both Windows 10 and WSL Ubuntu in FoxIO's own sample. 65535 covers both Ubuntu and Mac OSX/iPhone. On this data the window size alone separates almost nothing.
The option list carries more. FoxIO states two specific rules: "Microsoft Windows does not utilize TCP Option 8 (timestamp), whereas all Unix-based operating systems do. iOS ends with a TCP Option 0 (End of list) whereas other operating systems do not." The sample rows are consistent with both, the Windows row carrying no 8 and the Mac and iPhone row ending in 0.
The labels are coarse too. The same string, 64240_2-1-3-1-1-4_1460_8, is labeled Windows 11 in FoxIO's README and Windows 10 in its sample CSV. For anything fuller, FoxIO points at ja4db, which it describes as under very active development.
And the values drift with OS releases rather than sitting still per vendor. p0f, which FoxIO credits as one of JA4T's inspirations alongside Hershel+ and gait, records a Windows 7 or 8 initial window of 8192 in its own signature format. Different format, different era, and not the 64240 FoxIO now publishes for Windows 10. Treat any OS-to-JA4T mapping, the five rows above included, as a snapshot of specific OS versions at a specific time.
Why do two tools produce different JA4T values for the same SYN?
Because JA4T has no text specification. FoxIO has never published a technical_details/JA4T.md, and an issue opened on the specification repository on 2026-07-07, still open, documents the consequences with file and line citations into the three reference implementations: "The technical specification for JA4T is severely lacking in the repo."
The divergences it records are exactly the ones that break a cross-tool comparison.
- A SYN with zero TCP options. Rust joins an empty vector and leaves the field empty, producing something like
64240__0_0. Wireshark and Zeek emit00. - Absent MSS or window scale. Rust prints unpadded, Wireshark and Zeek zero-pad to two digits. The same SYN is
65535_00_00_00in Wireshark and Zeek but65535__0_0in Rust. - A duplicated MSS or window-scale option. Rust takes the first occurrence. Wireshark and Zeek overwrite on each, so the last one wins.
- ECN-flagged SYNs. Rust accepts them. Wireshark and Zeek require exact SYN flags and silently produce no fingerprint at all for an ECN-capable client.
The general form of the second bullet is that there is no universal padding rule to cite. Before comparing two JA4T strings, establish which implementation produced each one.
How are JA4 and JA4T collected?
JA4 needs the TLS ClientHello, which is cleartext, so anything terminating or observing the handshake can compute it: CDNs, load balancers, WAFs, reverse proxies, identity providers. JA4T needs raw TCP SYN header fields, which an ordinary userland HTTPS server never receives.
The maintainers of FoxIO's own nginx module put the constraint directly: "The nginx module can compute L7 fingerprints (JA4 / JA4H / etc.) in user space, but TCP-layer fingerprints and latency metrics need data nginx can't access: SYN/SYN-ACK TCP options, observed TTL/Hop Limit, and handshake packet timestamps." Their proposed answer is an eBPF collector feeding SYN metadata back up to nginx.
So JA4T comes from one of four places: Zeek with FoxIO's official script, Wireshark or tshark with FoxIO's plugin, F5 iRules on BIG-IP, or a third-party eBPF or nftables collector written specifically because userland servers cannot see SYN options. Cloudflare's entry in FoxIO's tool-support table credits it with JA4 only, which is consistent with an HTTPS-terminating edge not exposing raw SYN capture.
The gap shows up in the data. In FoxIO's public JA4+ database, records routinely carry a populated ja4_fingerprint and a null ja4t_fingerprint. One sample record labels the application SemrushBot with ja4_fingerprint t13d301000_01455d0db58d_5ac7197df9d2 and ja4t_fingerprint: null.
Neither fingerprint needs JavaScript or cookies. Both are derived from connection setup packets, the SYN and the ClientHello, which arrive before any HTTP request is parsed.
Can JA4 and JA4T be spoofed?
The TLS layer, yes, with off-the-shelf tooling. The TCP layer is harder, and the honest reason is that the widely used TLS impersonation tools do not attempt it.
uTLS is a fork of Go's crypto/tls built for exactly this. Its own scope statement: "Handshake is still performed by 'crypto/tls', this library merely changes ClientHello part of it and provides low-level access." Its documentation recommends using multiple fingerprints including randomized ones rather than relying on a single one, and concedes that "there's a small chance that generated fingerprint won't work, so you may want to keep generating until a working one is found."
curl-impersonate is a modified curl build that, in its own description, "can impersonate the four major browsers: Chrome, Edge, Safari and Firefox" with "TLS and HTTP handshakes that are identical to that of a real browser." Its README is also candid about the limits of that claim, noting that the original Safari fingerprints in the upstream fork are not correct, tracked as a known bug.
Both tools scope the claim to the TLS and HTTP layers. Neither claims to alter window size, option order, MSS or window scale, which the host operating system's kernel produces below the socket API these userland tools sit on. So a client can present a perfectly Chrome-shaped JA4 while its JA4T still reflects the machine it is actually running on. FoxIO treats that gap as the point of layering the methods: "it is the combination of JA4+ fingerprints that can facilitate creating detection and blocking rules with no false positives."
Two caveats belong on that, in both directions. No controlled published study measures how exactly uTLS or curl-impersonate reproduce a target's JA4 hash, so the scope above comes from the projects' own documentation rather than independent testing. And the absence of a TCP-layer claim from these tools is not proof that TCP characteristics cannot be shaped. It establishes that the common tools do not shape them.
What else is in the JA4+ suite?
- JA4S, TLS server response. The ServerHello depends on the ClientHello that prompted it, so the same client fingerprint always draws the same JA4S from the same server application.
- JA4H, HTTP client, computed per request:
abfingerprints the application and method,cthe cookie fields,dthe individual user. - JA4L and JA4LS, client-to-server and server-to-client latency and distance from the first handshake packets, plus the observed TTL, which buckets loosely by OS family: around 128 for Windows, 64 for Mac, Linux, phones and IoT, 255 for Cisco, F5 and most networking gear.
- JA4X, how an X.509 certificate was generated rather than what the certificate contains.
- JA4SSH, the SSH session on a rolling window, 200 packets by default, not the SSH application. FoxIO points to HASSH for that.
- JA4T and JA4TS, TCP client and TCP server. JA4TS is built from the SYN-ACK, and because servers adapt the SYN-ACK to whatever the client offered, one server can produce several JA4TS values.
- JA4TScan, active scanning: one SYN carrying all common TCP options, no answer to the SYN-ACK, then the observed retransmission delays appended as an extra section, prefixed with
Rif an RST appears. Recorded against an Epson printer:28960_2-4-8-1-3_1460_3_1-4-8-16. - JA4D and JA4D6, DHCP and DHCPv6.
Is JA4 open source?
JA4 is. Most of JA4+ is not, and the split decides what you are allowed to build.
JA4 TLS Client Fingerprinting is released under BSD 3-Clause, deliberately matching JA3's licence so that JA3 adopters can move across without a licensing conversation, and FoxIO states it holds no patent claims on it and is not planning to pursue any. Everything else, JA4S, JA4L, JA4LS, JA4H, JA4X, JA4SSH, JA4T, JA4TS, JA4TScan and future additions, is licensed under the FoxIO License 1.1, which FoxIO describes as permissive for most use cases including academic and internal business purposes but "not permissive for monetization." All JA4+ methods are patent pending. Cloudflare's own summary of the split matches, and the third-party eBPF JA4T collector flags the same limitation in its README before any code. If JA4T is going into something you sell, that is a licensing question before it is an engineering one.
Frequently Asked Questions
JA3 hashes its input with MD5, producing one 32-character digest with no readable structure. JA4 produces three underscore-separated sections: a readable prefix carrying transport, TLS version, SNI presence, cipher and extension counts and ALPN, followed by two 12-character truncated SHA-256 hashes. The substantive change is that JA4 sorts ciphers and extensions by hex value before hashing, so randomized ordering no longer changes the value, which is what defeated JA3 when attackers randomized cipher order and when Chrome began permuting extension order.
