Reading your handshake.
▸▾ About TLS fingerprinting
What a TLS fingerprint is
Before any HTTP request exists, before a single byte is encrypted, your client sends a ClientHello. It announces which TLS versions it speaks, which cipher suites it will accept, which extensions it supports, which elliptic curves it knows — and it sends all of that in a particular order.
That order is not standardised. It falls out of how the underlying library was written and configured, so Chrome’s BoringSSL, Firefox’s NSS, Go’s crypto/tls and Python’s OpenSSL bindings each produce a recognisably different ClientHello. Hash the interesting fields and you get a short string that identifies the TLS stack — not the person, not the machine, but the software doing the talking. It is what a server sees before it has any opportunity to trust or distrust your User-Agent.
The three fingerprints shown above
JA4
The current standard, from FoxIO. Partly readable: t13d1516h2 is TLS 1.3 over TCP, SNI present, 15 ciphers, 16 extensions, ALPN h2. Two truncated SHA-256 halves follow, over the sorted cipher and extension lists. How JA4 is built →
JA3
The 2017 original from Salesforce: an MD5 over version, ciphers, extensions, curves and point formats, in the order sent. Still everywhere in tooling, and no longer stable for browsers since Chrome began shuffling its extension order in 2023. How JA3 is built →
Akamai HTTP/2
Not TLS at all. SETTINGS in the order sent, the connection WINDOW_UPDATE, PRIORITY frames, and the order of the :method :authority :scheme :path pseudo-headers. A separate layer, defeated separately. How Akamai HTTP/2 is built →
What people use this for
Checking an impersonation library
curl-impersonate, curl_cffi, tls-client and rnet all claim a target browser. Whether the build you installed still matches it after a dependency bump is an empirical question. Verify it →
Understanding why a client is flagged
A request whose User-Agent says Chrome while its handshake says Python is the loudest signal in bot detection, and it is usually unintentional. What a mismatch looks like →
Writing or testing detection rules
Building WAF rules, fraud heuristics or JA4-based blocklists needs ground truth for what real clients emit, before you write a rule that fires on all of them.
Catching drift in CI
A fingerprint changes when your TLS stack changes, including when you did not mean to change it. Pin one →
Common questions
- What is a TLS fingerprint?
- A short string derived from the TLS ClientHello your client sends before any HTTP request exists. It hashes which cipher suites, extensions and elliptic curves were offered, and in some schemes the order they were offered in. Because those choices come from the TLS library rather than from anything you configure per-request, the result identifies the software making the connection.
- How do I check my own JA3 and JA4 fingerprint?
- Load this page in the client you want to measure, or call the API from it: curl -s https://kittens.sh/api/clean returns the JA3 hash, the JA4 fingerprint and the Akamai HTTP/2 hash as JSON. The response always describes the connection it arrived on, so whatever you call it with is what gets measured.
- Is JA3 still reliable?
- Not for identifying browsers. JA3 hashes the extension list in the order sent, and Chrome began randomising that order in 2023, so one browser produces a different JA3 on every connection. It remains useful for clients that do not randomise, which includes most scripted tooling, and it is still widely present in threat intelligence feeds. JA4 sorts the lists before hashing and is stable across the same clients.
- Why does my User-Agent not match my TLS fingerprint?
- Because the two are set in different places. The User-Agent is a header your code chooses freely; the fingerprint falls out of the TLS library actually performing the handshake. Setting a Chrome User-Agent on a Python requests session changes the header and nothing about the handshake, so the connection announces Python while the header claims Chrome. Servers that compare the two treat the disagreement as a strong signal.
- Can I change my TLS fingerprint?
- Yes, but not by setting headers. It requires a client that lets you control the handshake itself: curl-impersonate and curl_cffi ship browser-matched builds, and libraries such as tls-client and rnet expose the cipher and extension ordering directly. Whether a given build still matches its target after a dependency update is the thing worth checking empirically.
- Does kittens.sh store anything?
- No. The fingerprint is computed from the live connection and returned on that same connection. Nothing is written to disk, responses are never cached, and there is no key or account to create.