Every HTTPS connection begins with a ClientHello: the browser or tool announces which TLS version, cipher suites, extensions and elliptic curves it supports. That list is remarkably specific to the software making the request, which is why it can be hashed into a fingerprint and used to tell a real browser apart from an attack tool, before a single HTTP request is parsed.
JA3 was the first widely used version of this idea. It hashes the TLS version, the cipher list, the extension list, the curves and the point formats, in the order the client sent them. For years it worked well. Then attack tooling learned to shuffle that order.
What we measured
During a booter attack against our own edge we logged every ClientHello. The result was stark: 12,664 handshakes produced 10,789 unique JA3 fingerprints, roughly one distinct fingerprint per connection. A denylist keyed on JA3 is worthless against that, because by the time you ban a hash it has already been retired.
The same handshakes produced 11 unique JA4 fingerprints. Not eleven thousand. Eleven. One of them accounted for 10,692 of the connections.
The measurement
12,664 ClientHellos. 10,789 distinct JA3 fingerprints. 11 distinct JA4 fingerprints. Same traffic, same moment, two ways of hashing it.
Why the two disagree so violently
JA3 hashes the cipher and extension lists in transmission order. JA4, published by FoxIO, sorts them before hashing. That single design decision is the difference between the two numbers above.
Modern tooling built on libraries such as uTLS can emit a genuine, well-formed ClientHello while shuffling the order of ciphers and extensions on every connection. The client is still the same program with the same capabilities, but every permutation produces a completely different JA3 hash. Sort the lists first and the permutations collapse back into one value.
- JA3 answers: what did this client send, and in what order?
- JA4 answers: what is this client capable of, regardless of how it arranged the list?
- Order is trivially randomized by the attacker. The capability set is not, because changing it means changing what the tool actually supports.
The attacker was imitating Chrome
The detail that made this attack interesting was not the randomization. It was how close the tool got to a real browser. JA4 splits into three parts, and the middle part is a hash of the sorted cipher list. The attack traffic produced exactly the same cipher hash as real Chrome: the tool had copied Chrome's cipher suite list verbatim.
What it did not copy correctly was the extension count. Real Chrome presented 14 extensions; the tool presented 12. That difference lands in the third part of the JA4 string, so the complete fingerprints diverged even though the cipher halves matched. Chrome was served, the tool was refused.
Why imitation is hard to finish
Copying a cipher list is a five-minute job. Reproducing a browser's full extension set, consistently across versions and platforms, means reimplementing a large part of the browser's TLS stack. Most tools stop short, and the gap is where they are caught.
What a fingerprint is good for, and what it is not
A TLS fingerprint is an identity signal, not a verdict. It is genuinely useful for two things: recognising known attack tooling before you spend CPU on its HTTP request, and confirming that a client which claims to be a browser actually handshakes like one. It is not a general-purpose blocklist.
The failure mode is worth stating plainly, because we walked into it ourselves. Millions of real people share a handful of browser fingerprints. If you rate-limit or ban an aggregate fingerprint you do not ban an attacker, you ban every Chrome user on the site at once. We removed exactly that mechanism after it produced a false positive against real visitors on a busy domain. A fingerprint should gate per-connection decisions, never a global ban on a value that legitimate users share.
Where it sits in a real defence
Fingerprinting is one layer and it is cheap, which makes it a good first one. At our edge it runs during the TLS handshake, before any HTTP parsing, so recognised attack tooling is dropped at the lowest possible cost. Behind it sit the layers that do the heavy lifting against distributed traffic: per-IP rate limits generous enough not to punish carrier-grade NAT, a browser challenge that engages automatically when a domain's request rate spikes, IP, country and network rules, and a WAF on the requests that survive.
No single one of those stops a serious attack on its own. The fingerprint layer's specific contribution is that it removes the cheapest, most obvious tooling for almost no CPU, and it tells you what you are actually being hit with, which is worth knowing before you decide what to do next.
If you want to try this yourself
- Log the ClientHello, not just the request. Most of the signal is gone by the time your application sees an HTTP header.
- Compute JA4 rather than JA3 if you have to pick one. Order-insensitive hashing is what survives contact with modern tooling.
- Never ban an aggregate browser fingerprint. Use it to identify, then decide per connection and per source.
- Watch the distinct-fingerprint count during an attack. A ratio near one fingerprint per connection is itself a strong signal that you are facing randomized tooling.
The honest summary: JA3 alone has been overtaken by the tooling it was built to catch. JA4 is not a silver bullet either, but the sorted-list design makes it hold up where its predecessor collapsed, and the numbers above are what that difference looks like under a real attack.