Skip to content

Research note 007

TLS Fingerprinting: How a Handshake Gives the Bot Away

Before a browser sends a single byte of HTTP, its TLS handshake has already announced what software is really making the connection. Here's how ClientHello fingerprinting works, why it's tied to specific Chrome versions, and why a mismatched User-Agent is one of the cleanest tells in bot detection.

7 min read

A request can lie about almost anything. The User-Agent header can claim to be any browser on any operating system; it’s just a string the client chooses to send. But there’s one part of the conversation that happens before any of that, one that’s much harder to fake convincingly, because it’s not a claim — it’s a behavior. It’s the way the client says hello.

Every HTTPS connection opens with a TLS handshake, and the very first message is the ClientHello: the client’s opening statement of how it wants to establish an encrypted channel. Buried in that message is a surprisingly precise signature of what software is actually making the connection — and comparing that signature against what the client claims to be is one of the earliest, cheapest, and most reliable signals in bot detection.

What’s actually in a ClientHello

When a client initiates TLS, it doesn’t just say “let’s encrypt this.” It sends a detailed proposal:

  • The TLS versions it supports.
  • The list of cipher suites it’s willing to use — and crucially, in a specific order of preference.
  • A set of extensions — features like SNI, ALPN, supported elliptic curves, signature algorithms, and more — also in a particular order.
  • Supporting details: the named groups (curves) it supports, point formats, key shares, and so on.

Here’s the key insight: none of this is standardized down to the exact bytes. The TLS specification says what’s allowed, not exactly how a given piece of software must assemble its ClientHello. So different implementations make different choices — which ciphers, in which order, which extensions, arranged how. A real Chrome build produces a characteristic ClientHello. A stock Python requests script, a Go HTTP client, an OpenSSL command line, a Node fetch — each produces a distinctly different one. The handshake is a dialect, and the dialect betrays the speaker.

JA3, JA4, and turning a handshake into a hash

To make this usable at scale, you summarize the salient fields of the ClientHello into a compact fingerprint.

JA3, introduced by Salesforce, was the first widely-adopted version: it concatenates the TLS version, cipher list, extension list, curves, and point formats, then hashes the result. Two connections with the same JA3 are almost certainly the same kind of client software. For years this was the workhorse of TLS fingerprinting.

JA3 had a weakness, though, and it’s an instructive one. Because it hashed the fields in order, anything that shuffled the order broke it — and that’s exactly what happened when Chrome began randomizing the order of its TLS extensions in 2023. Suddenly the same Chrome browser produced a different JA3 on every connection, defeating naive fingerprinting overnight.

JA4 (and the broader JA4+ suite from FoxIO) is the modern successor, built to survive that. It sorts the lists before hashing so reordering doesn’t matter, handles TLS 1.3 cleanly, incorporates additional signals like ALPN, and is structured to be partly human-readable rather than an opaque hash. The move from JA3 to JA4 is itself a small lesson in this whole field: a fingerprinting scheme is a fixed target, the thing it measures evolves, and the scheme has to evolve with it.

Why it’s tied to specific Chrome versions

This is where it gets precise. A browser’s ClientHello isn’t just “Chrome-like” in general — it’s specific to a version, because Chrome’s TLS behavior changes as the browser evolves, and each change moves the fingerprint.

A few examples of what shifts it:

  • GREASE. Chrome deliberately inserts random reserved values into its cipher and extension lists (a mechanism called GREASE, standardized in RFC 8701) to keep the ecosystem from ossifying around a fixed set. Fingerprinting has to normalize away these random values, or every connection looks unique — a subtlety that trips up naive implementations.
  • Extension order randomization. As noted, modern Chrome shuffles extension order per-connection, which is precisely what pushed the industry from order-sensitive JA3 to order-insensitive JA4.
  • New capabilities over time. Chrome adds and changes features — post-quantum key exchange (the X25519Kyber, later ML-KEM, key shares), Encrypted Client Hello, changes to supported ciphers and curves. Each of these alters the shape of the ClientHello, so a given Chrome version has a recognizable footprint, and a new release produces a new one.

The practical upshot for a defender is that “what does a real Chrome handshake look like?” is not a fixed answer — it’s a moving set that you have to track release by release. Which is a burden, but also an opportunity: because the legitimate fingerprints are a known, evolving set, anything outside that set is immediately interesting.

The cleanest tell: when the story doesn’t match

The real power of TLS fingerprinting isn’t identifying a client in isolation — it’s coherence checking against everything else the client says.

Consider a request whose User-Agent header proudly declares “Chrome 128 on Windows 10.” That’s a claim. Now look at its TLS fingerprint. If it matches the known ClientHello of that Chrome version, the story holds. But if the fingerprint instead matches a Go HTTP library or a Python client, you’ve caught a contradiction that the client can’t easily explain away. The header says browser; the handshake says script. Real browsers don’t lie about their own TLS stack, because the TLS stack is the browser — so a mismatch between the claimed identity and the actual handshake is a strong, early, passive signal that something is impersonating a browser it isn’t.

And it’s early and cheap in a way that matters. This happens at connection time, before a single HTTP request is processed, before any JavaScript runs. It costs the defender almost nothing and it filters out a huge amount of low- and mid-effort automation that never bothered to match its TLS layer to its headers.

The impersonation arms race

Of course, the moment TLS fingerprinting became a common defense, tooling emerged to defeat it — libraries and modified stacks designed to mimic a real browser’s ClientHello exactly (the open-source utls and curl-impersonate projects are well-known examples, built for legitimate testing and research as much as anything). So sophisticated automation increasingly presents a TLS fingerprint that genuinely matches a real Chrome.

But mimicry is harder than it looks, and understanding why is understanding the defense. A browser’s fingerprint isn’t one value — it’s a consequence of an entire coherent stack. To impersonate convincingly, the attacker has to match not just the JA3/JA4 hash but the specific Chrome version, and have that agree with the User-Agent, and agree with the HTTP/2 fingerprint (the way the client frames HTTP/2, another distinctive signature), and agree with everything JavaScript later reports about the browser environment. Get any one of these out of step — a TLS fingerprint for Chrome 120 with a User-Agent claiming Chrome 128, a perfect Chrome handshake paired with an HTTP/2 fingerprint that isn’t Chrome’s — and the incoherence is the tell. The defender’s advantage is that a real browser is automatically consistent across all these layers, while an impersonator has to manually keep every layer in sync as all of them keep changing.

What a defender should take from this

  • Use it as a coherence signal, not a verdict. TLS fingerprinting is at its strongest comparing the claimed identity against the actual handshake, and cross-checking against HTTP/2 and higher-layer signals. A fingerprint alone identifies a category of software; the contradiction is what identifies a liar.
  • Keep your fingerprint database current — this is a treadmill too. Browsers change their handshakes every few releases. A static set of “known-good” fingerprints goes stale fast and starts flagging real users. Tracking the legitimate, evolving set is ongoing work, not a one-time import.
  • Account for legitimate variation. GREASE, extension randomization, corporate TLS-inspection middleboxes, and VPNs can all alter or obscure a ClientHello without any malicious intent. Normalize what should be normalized, and remember that a mismatch can be a proxy or a security appliance, not always an attacker.
  • Layer it. TLS fingerprinting is a superb early filter and a weak sole defense. It belongs at the front of a stack of corroborating signals — cheap enough to run on everything, never trusted enough to decide alone.

The handshake is the first thing a client says and the last thing most automation thinks to disguise properly. Long before the application layer, before a header or a rendered pixel, the way a connection opens has already hinted at whether there’s a browser behind it. Reading that hint well — and keeping up as the handshakes evolve — is one of the quietest and most durable tools in the detection kit.


This article is a defensive overview of TLS fingerprinting. It explains how the technique works and how detection uses it, and contains no methods or tooling guidance for impersonating or defeating any specific system.