JA3 and JA4 TLS Fingerprinting Explained for Web Operators
TL;DR. Every TLS handshake starts with a ClientHello whose contents are determined entirely by the client library. JA3 (2017, Salesforce) and JA4 (2023, FoxIO) hash that ClientHello into a short string that uniquely identifies the library family — Chrome vs Firefox vs curl vs the Go standard library — long before HTTP is exchanged. Bot detection, fraud scoring, ad-tech, and incident response all rely on it.
What goes into the hash
A TLS ClientHello is a structured packet a few hundred bytes long. The mandatory fields — TLS version, cipher suite list, supported extensions, named groups, EC point formats — are filled in by the TLS stack, and the order in which they appear is generally consistent within a given library and version. Two installs of the same Chrome major version produce essentially the same ClientHello on the wire, while curl and Python and Go all produce visibly different ones.
JA3 captured that with a deceptively simple recipe: concatenate the TLS version, cipher suites, extensions, elliptic curves, and EC point formats — each as a comma-separated list of integers — join the lists with commas, and MD5 the result. The output is a 32-character hash you can log, cluster on, and join against threat-intel feeds.
JA4 keeps the spirit and fixes a number of practical issues with JA3: it's human-readable instead of opaque, it splits the hash into multiple fields so individual components are diffable, and it ignores GREASE values which would otherwise rotate the hash unhelpfully. It also adds companion hashes for HTTP/2 and HTTP/3, so a single client is described by JA4 (TLS) + JA4_H (HTTP) + JA4_X509 (the server certificate) — together far more resistant to forgery than JA3 ever was.
The pieces, one client at a time
The widget below lets you pick a representative client and inspect what goes into its JA3 / JA4. Watch how different the “cipher suites” line is between curl and Chrome — that single field alone is enough to tell “a browser” from “a script trying to look like one”.
Pick a client to see what its TLS ClientHello looks like and how that hashes down to a JA3 / JA4 fingerprint. The data is illustrative \u2014 the format is faithful, but treat individual hash values as examples for the article rather than ones to cross-reference against your own captures.
771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-21,29-23-24,0cd08e31494f9531f560d64c695473da9t13d1517h2_8daaf6152771_b0da82dd1658b0da82dd16580x0303 (TLS 1.2 record) negotiating up to TLS 1.3Chromium sends the legacy 0x0303 record version even for TLS 1.3, signalled via the supported_versions extension.
TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, ECDHE-ECDSA-AES128-GCM, …TLS 1.3 ciphers first, then a long tail of TLS 1.2 ECDHE/RSA suites for backwards compatibility.
server_name, status_request, supported_groups, ec_point_formats, signature_algorithms, application_layer_protocol_negotiation, padding, …The order matters — different browsers emit the same extensions in different sequences and that order is part of JA4.
X25519, secp256r1, secp384r1Chromium leads with X25519; this single field is one of the strongest discriminators between Chromium and Firefox.
h2, http/1.1HTTP/2 first, HTTP/1.1 fallback. The full ALPN list ends up in the JA4_h component.
Three patterns to memorise: browsers send GREASE, libraries don't; browsers ship a curated cipher suite list, libraries hand you whatever OpenSSL has compiled in; browsers prefer HTTP/2 ALPN, default Python / curl / Java clients don't. Knowing those three is enough to spot 90 % of unsophisticated bots from the ClientHello alone.
Reference: who fingerprints to what
The table below catalogues the families you're likely to see at the edge of a production service: real browsers, legitimate bots like crawler agents, common HTTP libraries, and the impersonation tools (utls, curl-impersonate, puppeteer-real-browser) attackers reach for when they actually try.
A starter set of common fingerprints worth recognising. Filter by category to compare browsers, libraries, CLIs and known impersonators side-by-side.
| Client | JA4 | Category | Behaviour |
|---|---|---|---|
Chrome 126 desktop | t13d1517h2_8daaf6152771_b0da82dd1658 | Desktop browser | Canonical legitimate desktop user |
Firefox 127 desktop | t13d1715h2_5b57614c22b0_3d5424432f57 | Desktop browser | Canonical legitimate desktop user |
Safari 17.4 macOS | t13d1314h2_75ee30e3b97c_d8f1abae5841 | Desktop browser | Canonical legitimate desktop user |
Edge 126 desktop | t13d1517h2_8daaf6152771_b0da82dd1658 | Desktop browser | Chromium twin — same JA4 as Chrome |
Chrome Mobile 126 Android | t13d1516h2_9bd05cabb112_47df1fa9d3b1 | Mobile browser | Subtly different ALPN order from desktop |
Safari iOS 17.4 | t13d1214h2_b1a4e62cfaa1_71e8d54c4eb2 | Mobile browser | Distinct from macOS Safari at the JA4 level |
curl 8.7 + OpenSSL 3.2 | t13d3112h2_e8f1e7e78f70_1d3a9b8b9f7c | Command-line tool | Default scraping suspect; long cipher list, no GREASE |
Python requests 2.32 + urllib3 2.2 | t13d2914h1_0d2e63eab27a_9cf2f56d3c91 | HTTP library | HTTP/1.1 ALPN gives it away instantly |
Go net/http Go 1.22 | t13d1311h2_4f156b3f1e84_3aa9e8f88c7e | HTTP library | Static across Go versions; trivially detectable |
Java HttpClient 21 | t13d1310h2_07b69283c5dd_1edc5f47caee | HTTP library | Used by many enterprise crawlers and monitors |
utls (Chrome-impersonating) parrot 126 | t13d1517h2_8daaf6152771_b0da82dd1658 | Bot / impersonator | Forged Chrome JA4 — only HTTP/2 frame fingerprint distinguishes it |
curl-impersonate chrome116 | t13d1517h2_8daaf6152771_5b6f4ad3e1f2 | Bot / impersonator | Forged ClientHello, but JA4_h drift outs it |
puppeteer-real-browser 1.x | t13d1517h2_8daaf6152771_b0da82dd1658 | Bot / impersonator | Runs the real Chromium binary — JA4 indistinguishable from human Chrome |
Slack link previewer Slackbot 1.0 | t13d3014h1_5c3ec2c9c00f_2f1d1c1c6cf5 | Bot / impersonator | Legitimate bot; harmless but should not count as a human visit |
GoogleBot crawler 2.1 | t13d2914h2_ad58e1e5c9c8_e1b1b7b0c6f0 | Bot / impersonator | Legitimate crawler; whitelist by reverse-DNS + JA4 combo |
Using JA4 in production
Where to collect it
JA4 can be computed anywhere you can see the raw ClientHello: nginx with the ngx_http_ja4_module, HAProxy with the JA4 patch, Envoy via a TLS inspector filter, or off-box on the mirror port of a load balancer with Suricata, Zeek, or any of the dedicated JA4 collectors. Cloudflare, Akamai, Fastly, and most WAFs expose it as a request header you can log without running any TLS code yourself.
What to do with it
- Allow-list legitimate bots. Combine JA4 with reverse-DNS verification. Googlebot's JA4 + a reverse DNS in
*.googlebot.comis enough to whitelist with confidence. - Cluster anomalous traffic. A burst of requests sharing the same JA4 from thousands of residential IPs is almost always a botnet. The JA4 reveals the malware family the operator built the client around.
- Feed your fraud score. A “Chrome User-Agent” combined with a Python-shaped JA4 is the cheapest possible signal that a bot is lying about itself. It costs nothing to compute and is incredibly cheap to act on at the WAF layer.
- Hunt incidents. Threat-intel feeds publish JA4 hashes for known malware droppers, C2 frameworks, and credential stuffers. Cross-referencing your edge logs against those feeds catches campaigns that haven't shown up in IP-based reputation yet.
Don't block on JA4 alone
JA4 narrows the population, it doesn't identify individuals. Many bots now use tools like utls or curl-impersonate that ship hard-coded Chrome-shaped ClientHellos. The honest defensive stance is to combine JA4 with browser-level signals (HTTP/2 frame fingerprint, behavioural timing, attestation), not to make it the only gatekeeper.
How attackers evade
- Use a real browser. The unbeatable evasion is to run a real Chromium binary with Puppeteer or Playwright — JA4 is then indistinguishable from a human. Defending against that needs behavioural detection, not network signatures.
- utls / boringssl impersonation. A library that forges the ClientHello byte for byte. The give-away is downstream: HTTP/2 SETTINGS frames and frame ordering differ from the impersonated browser, and JA4_H catches that drift.
- Rotating fingerprints. Sophisticated bots rotate between several captured ClientHellos per session. Aggregating JA4 distribution over time, not per request, is what spots the pattern.
See your own fingerprint
Our infrastructure collects JA3 / JA4 on every request to IP Details and surfaces it alongside the IP geolocation and ASN. If you are tuning a bot-detection pipeline, that page is a fast way to see what your client currently looks like on the wire.