Quick answer
TLS fingerprinting identifies what software is making a connection by looking at how it negotiates encryption, rather than at what it claims to be. Every client sends a Client Hello listing the cipher suites, extensions and curves it supports, in a particular order, and that combination differs between Chrome, Firefox, curl and a Python script. Hashing it produces a fingerprint (JA3, or the newer JA4) that reveals the underlying library even when the user-agent header says something else entirely.
01How a TLS fingerprint is built
Before any encrypted data flows, the client sends a Client Hello announcing what it supports: TLS versions, cipher suites in preference order, extensions, elliptic curves and point formats. Nothing in it is secret and nothing in it identifies a person. It describes the software, and it is the opening move of the TLS handshake that every HTTPS connection begins with.
JA3, the method that popularised the idea, reads five of those fields as decimal values, keeps them in the order the client sent them, joins them with commas and hyphens, and hashes the string with MD5 into a 32-character fingerprint. Values reserved for GREASE are skipped, so browsers that use GREASE still map to a single hash. Two clients built on the same library and version produce the same hash; a different library produces a different one.
The important property is timing. A server learns something about the client before it has seen a single HTTP header, including the user-agent the client might be lying in.
TLSVersion,Ciphers,Extensions,EllipticCurves,CurveFormats
771,4865-4866-4867-49195-49199,0-23-65281-10-11,29-23-24,0
│ │ │
│ │ └─ curves
│ └─ extension list, in the order sent
└─ cipher suites, in the client's preference order
→ MD5 → e195ee6c10d79ea3730f8bf6fca7bcf102Why JA4 replaced JA3
JA3 hashes extensions in the order the client sends them, and that order stopped being stable. In November 2022 the Chromium team announced that Chrome would randomise the order of its Client Hello extensions, explaining that a fixed order encourages servers to fingerprint Chrome and then assume specific behaviour. Once that rolled out, the same browser produced a different JA3 hash on almost every connection.
That is a different mechanism from GREASE, which is often blamed for the same effect. GREASE, standardised in RFC 8701, has clients advertise reserved dummy values so that servers stay tolerant of values they do not recognise. JA3 and JA4 both ignore those values.
JA4, published by FoxIO, sorts cipher suites and extensions before hashing, leaves the SNI and ALPN extensions out of the extension hash, and puts a readable summary in front of two truncated SHA-256 hashes. The protocol, TLS version and counts are visible without a lookup table, which makes it far easier to reason about and to debug. The original JA3 repository was archived in May 2025.
| JA3 | JA4 | |
|---|---|---|
| Hash | One MD5 of the whole string | Readable prefix plus two truncated SHA-256 hashes |
| Extension order | Included, so randomisation breaks it | Sorted, so randomisation does not matter |
| GREASE values | Ignored | Ignored |
| Transport | TLS over TCP | TLS, QUIC and DTLS, marked in the prefix |
| Family | JA3S for the server reply | JA4S, JA4H for HTTP, JA4L and others |
03Reading a JA4 fingerprint
The JA4 specification uses the fingerprint below as its worked example. The first part is plain text; the other two are hashes that only match exactly. Signature algorithms are added to the third hash in their original order, while ciphers and extensions are sorted.
| Part | Value | Meaning |
|---|---|---|
| Protocol | t | TLS over TCP (q would mean QUIC, d DTLS) |
| Version | 13 | Highest supported TLS version, here 1.3 |
| SNI | d | The client sent a domain name (i would mean an IP address) |
| Cipher count | 15 | Cipher suites offered, GREASE excluded |
| Extension count | 16 | Extensions offered, GREASE excluded |
| ALPN | h2 | First and last character of the first ALPN value, 00 if none |
| Cipher hash | 8daaf6152771 | Truncated SHA-256 of the sorted cipher list |
| Extension hash | e5627efa2ab1 | Truncated SHA-256 of the sorted extensions plus signature algorithms |
t13d1516h2_8daaf6152771_e5627efa2ab1
└───a────┘ └─────b────┘ └─────c────┘
a = readable prefix b = cipher hash c = extension hash04What it actually catches
The classic case is a script claiming to be a browser. A Python request sending a Chrome user-agent negotiates like Python, because it is Python, and its fingerprint matches no browser build. The contradiction between claimed identity and observed handshake is the signal.
It also groups activity. If one fingerprint is shared by thousands of addresses that all appeared in the last minute and all request the same login endpoint, that is probably one tool being run at scale, which is the usual shape of credential stuffing and of bulk web scraping.
The idea extends past TLS. Akamai researchers showed in 2017 that HTTP/2 clients can be passively fingerprinted from differences in how they implement the protocol, and JA4H applies the approach to the HTTP request itself. A client whose HTTP behaviour contradicts its TLS handshake is telling two different stories.
| Observation | What it suggests | What it can justify on its own |
|---|---|---|
| Browser user-agent, handshake of a scripting library | A script impersonating a browser | Strong evidence, still best combined with other signals |
| Browser user-agent, matching browser handshake | A real browser, or a headless one | Nothing: this is what most visitors look like |
| Library handshake with an honest user-agent such as curl | Automation that is not hiding | Your normal policy for API and automated clients |
| A handshake no current browser ships | Old software, embedded devices or tooling | Closer review, not an automatic block, because old devices exist |
| One fingerprint, many new addresses, one endpoint | One tool run from many networks | Investigation, together with behaviour and request rate |
05The limits, and the trap
A fingerprint is shared, not unique. Every Chrome user on the same version and platform produces the same value, which is good for privacy and a hard constraint for detection: a fingerprint identifies a class of software, never an individual.
That leads to a common and costly mistake. It is tempting to treat many requests sharing one fingerprint as evidence of a botnet, but legitimate browser traffic is extremely homogeneous, because most visitors run one of a handful of current browser builds. Real traffic can easily be more uniform than the attack you are trying to catch, which is why sound bot management treats a fingerprint as context rather than a verdict.
06Can it be evaded?
Yes, and increasingly easily. Libraries such as uTLS exist specifically to parrot the Client Hello of popular browsers, and a headless browser needs no impersonation at all because it genuinely is a browser.
This does not make fingerprinting useless. It raises the cost of an attack, which is the actual goal, and a forged handshake still has to agree with everything else the client sends. It does mean the fingerprint must be one input among many. Sound systems combine it with the other ways websites tell bots from humans, such as header coherence, behaviour, network reputation and the outcome of a proof-of-work challenge, and require several to agree before acting.
07How to look at a fingerprint yourself
The quickest way to understand the method is to look at real handshakes. Capture a few Client Hellos with tcpdump and open the file in Wireshark: version 3.6 and later show a JA3 field for each Client Hello, and version 4.2 and later add JA4.
The filter below matches TLS handshake records (content type 22) whose first message is a Client Hello (type 1) on port 443. It assumes the record starts at the beginning of the TCP payload, which holds for the first data segment of a connection, and it only matches IPv4, because tcpdump cannot index into a TCP header carried over IPv6.
It helps to place this in the OSI model: the handshake rides on TCP, and everything it reveals is known before HTTP begins. HTTP/3 is different, because QUIC carries its Client Hello inside protected Initial packets over UDP, so this TCP filter will not see it, although Wireshark can still decode those packets.
# TLS handshake record (0x16) carrying a Client Hello (0x01) on port 443, IPv4 only
tcpdump -ni eth0 -c 20 -w hello.pcap \
'tcp dst port 443
and tcp[((tcp[12:1] & 0xf0) >> 2)] = 0x16
and tcp[((tcp[12:1] & 0xf0) >> 2) + 5] = 0x01'
# open hello.pcap in Wireshark and filter on: tls.handshake.type == 1Questions
Frequently asked questions
Sources
Sources and verification
Capture filter checked with tcpdump 4.99.3 (tcpdump -d), the JA3 example hash recomputed with md5sum and the JA4 example hashes recomputed with sha256sum, on Debian 12, October 2026.
- 01JA4 technical details · FoxIO
- 02JA3 (salesforce/ja3, archived repository) · Salesforce
- 03Intent to Ship: TLS ClientHello extension permutation · Chromium blink-dev
- 04RFC 8701: Applying GREASE to TLS extensibility · IETF
- 05Display filter reference: Transport Layer Security (tls.handshake.ja3, tls.handshake.ja4) · Wireshark
- 06uTLS (refraction-networking/utls) · Refraction Networking
- 07Passive Fingerprinting of HTTP/2 Clients (white paper) · Akamai / Black Hat Europe 2017
Last updated