A JA4 fingerprint is a three-part string, a_b_c, computed from a TLS ClientHello. Part a is readable metadata (protocol, TLS version, SNI, cipher and extension counts, ALPN), part b is a 12-character SHA-256 of the sorted cipher list, and part c is a 12-character SHA-256 of the sorted extensions plus signature algorithms. If you searched for b0da82dd1658: it is a c section, and it belongs to Chromium. Specifically, it is what a Chromium-based client produces when it resumes a TLS 1.3 session while still using the older ALPS extension codepoint. The rest of this page shows how we know that, and how to decode any JA4 string yourself.
How a JA4 fingerprint is built: the a_b_c format
The official JA4 technical specification from FoxIO defines the fingerprint as one human-readable prefix and two truncated hashes, joined by underscores. GREASE values are ignored everywhere, and all hashes are lowercase.
| Section | Characters | What it encodes | Possible values |
|---|---|---|---|
| a | 1 | Transport | t = TLS over TCP, q = QUIC, d = DTLS |
| a | 2 | Highest TLS version (from supported_versions if present, else the ClientHello version) | 13, 12, 11, 10, s3, d2, 00 = unknown… |
| a | 1 | SNI present? | d = domain (SNI sent), i = no SNI (usually connecting to a bare IP) |
| a | 2 | Number of cipher suites (GREASE excluded) | 00–99 |
| a | 2 | Number of extensions (GREASE excluded, SNI and ALPN included) | 00–99 |
| a | 2 | First and last character of the first ALPN value | h2, h1 (http/1.1), h3, 00 = no ALPN |
| b | 12 | SHA-256 of cipher suites as 4-char hex, sorted, comma-joined | 000000000000 if none |
| c | 12 | SHA-256 of extensions (sorted, SNI 0000 and ALPN 0010 removed) + _ + signature algorithms in wire order | 000000000000 if none |
Two details trip people up. First, the extension count in part a includes SNI and ALPN, but the extension hash in part c excludes them, so the same client produces the same c whether it connects to a hostname or an IP. Second, signature algorithms are hashed in the order the client sent them, not sorted, so a client that reorders its signature preferences changes c even with identical extensions.
Decoding t13d1516h2_8daaf6152771_… piece by piece
t: TLS over TCP, not QUIC.13: the client offers TLS 1.3 as its highest version.d: an SNI extension was present, so the client was connecting to a domain name.15: 15 cipher suites after removing GREASE.16: 16 extensions after removing GREASE, counting SNI and ALPN.h2: the first ALPN value ish2, meaning the client prefers HTTP/2.8daaf6152771: the hash of this sorted cipher string, which you can reproduce with any SHA-256 tool:
printf '%s' "002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9" \
| sha256sum | cut -c1-12
# 8daaf6152771
That list is the three TLS 1.3 suites plus Chrome's twelve legacy TLS 1.2 suites. Every Chromium build we checked, from the FoxIO examples through a headless Chrome 148, still produces 8daaf6152771. So the b section tells you "Chromium-family TLS stack" and almost nothing about the version. Version differences show up in c.
One small caveat if you are reading the spec closely: its algorithm summary shows t13d1516h2_8daaf6152771_b186095e22b6, while its worked example hashes to e5627efa2ab1. We recomputed the worked example and got e5627efa2ab1, so treat the first string as illustrative.
What b0da82dd1658 means
b0da82dd1658 is the c section of a Chromium JA4. It is the hash of this exact string:
0005,000a,000b,000d,0012,0017,001b,0023,0029,002b,002d,0033,4469,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601
You can check it yourself:
printf '%s' "0005,000a,000b,000d,0012,0017,001b,0023,0029,002b,002d,0033,4469,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601" \
| sha256sum | cut -c1-12
# b0da82dd1658
Three extensions in that list identify the client:
0029(pre_shared_key). This is TLS 1.3 session resumption. It only appears when the client already has a ticket from an earlier connection to the same server. RFC 8446 §4.2.11 defines it. Without it, the same browser hashes to02713d6af862.4469(ALPS, original codepoint). Application-Layer Protocol Settings is a Chrome extension. BoringSSL added a new codepoint for it in September 2023 (0x44cd), and current Chrome sends the new one. So4469points to an older Chromium build or a tool that copies one.fe0d(Encrypted Client Hello). Chrome sends a GREASE ECH extension on ordinary connections.
FoxIO's own ja4plus-mapping.csv labels t13d1517h2_8daaf6152771_b0da82dd1658 and t13i1516h2_8daaf6152771_b0da82dd1658 as "Chromium Browser". The prefix arithmetic matches: 15 hashed extensions plus SNI plus ALPN gives 17 with SNI (d), and 16 without it (i). That also exposes an inconsistency you'll see repeated online. A string like t13d1516h2_8daaf6152771_b0da82dd1658 cannot come from a single real ClientHello: with SNI and ALPN both present, those 15 hashed extensions would have to produce a count of 17.
We also reproduced the value directly. On 2026-10-03, curl_cffi 0.16.3 with impersonate="chrome131" sent its first request to tls.peet.ws as t13d1516h2_8daaf6152771_02713d6af862. Its second request on the same session, which resumed the TLS session, came back as t13d1517h2_8daaf6152771_b0da82dd1658. In short, b0da82dd1658 means "a Chromium-style ClientHello, older ALPS codepoint, resumed session". That covers real older Chrome, Edge, Brave and Opera builds, embedded Chromium, and impersonation libraries pinned to a pre-switch Chrome profile. A JA4 alone cannot tell those apart.
Reference table: real JA4 values by client
The values below come from two sources only: FoxIO's published examples and mapping file, and our own captures against tls.peet.ws on 2026-10-03 (versions stated). Library values depend heavily on the TLS backend and its build options, so treat the library rows as examples, not universal constants. We left out rows we could not verify, including a real Firefox and Safari capture of our own.
| Client | JA4 | Source |
|---|---|---|
| Headless Chrome 148 (Linux), new connection | t13d1516h2_8daaf6152771_d8a2da3f94cd | Our capture, Playwright Chromium |
| Same browser, resumed session | t13d1517h2_8daaf6152771_b6f405a00624 | Our capture |
| Chromium, older ALPS codepoint, new / resumed | t13d1516h2_8daaf6152771_02713d6af862 / t13d1517h2_8daaf6152771_b0da82dd1658 | FoxIO mapping CSV; reproduced with curl_cffi chrome131 |
| Chrome over QUIC | q13d0312h3_55b375c5d22e_178839b6cec1 | FoxIO README |
| Mozilla Firefox (version not stated) | t13d1715h2_5b57614c22b0_7121afd63204 | FoxIO mapping CSV |
| Safari (version not stated) | t13d2014h2_a09f3c656075_14788d8d241b | FoxIO mapping CSV |
| curl 8.5.0 / OpenSSL 3.0.13 (Ubuntu) | t13d3112h2_e8f1e7e78f70_b26ce05bbdd6 | Our capture; identical on tls.peet.ws, BrowserLeaks and Scrapfly |
| Python requests 2.31 / urllib3 2.0.7, OpenSSL 3.0.13 | t13d3112h1_e8f1e7e78f70_b26ce05bbdd6 | Our capture |
| Go 1.24.1 net/http (default client) | t13d1311h2_f57a46bbacb6_e7c285222651 | Our capture |
curl_cffi 0.16.3, impersonate="chrome146" | t13d1516h2_8daaf6152771_d8a2da3f94cd | Our capture |
curl_cffi 0.16.3, impersonate="firefox147" | t13d1717h2_5b57614c22b0_3cbfd9057e0d | Our capture |
curl_cffi 0.16.3, impersonate="safari2601" | t13d2013h2_a09f3c656075_7f0f34a4126d | Our capture |
The patterns matter more than the individual rows:
- The c section often fingerprints the TLS library, not the application. curl and Python on the same OpenSSL 3 build share
b26ce05bbdd6. FoxIO's mapping file has a Python entry with a different prefix but the same c. Go clients in FoxIO's file sharee7c285222651with our Go 1.24 capture. - requests shows
h1. urllib3 2.x advertises onlyhttp/1.1in ALPN, so the a section tells a server it is not talking to a browser before any header is read. - A plain library is easy to spot by its counts alone. 31 ciphers (OpenSSL default) or 13 ciphers with 11 extensions (Go) looks nothing like the 15/16 pattern every current Chromium build sends.
- Resumption changes c for every browser. Firefox goes from 17 to 18 extensions and Safari from 13 to 14 on resumed sessions, in our curl_cffi captures. A detection rule that only allow-lists the fresh-connection value will misfire on real returning visitors.
Expect the Chrome row to keep moving. curl_cffi's newest chrome150 profile adds three signature algorithms, 0904, 0905 and 0906. Those are the ML-DSA codepoints from the IETF draft, and they turn c into 806a8c22fdea. We could not confirm that against a stable Chrome 150 build, so treat it as a preview, not a reference value.
Why JA4 sorts ciphers and extensions
JA4 sorts ciphers and extensions because Chrome deliberately randomises its extension order, and an order-sensitive fingerprint (JA3) stopped being stable. Chrome shipped TLS ClientHello extension permutation around Chrome 110 in early 2023. The goal was to stop servers and middleboxes from depending on a fixed layout. Fastly measured the effect: since JA3 hashes extensions in wire order, one Chrome install produces a different JA3 on practically every connection.
JA4 solves this by sorting before hashing. FoxIO published it as part of the JA4+ suite in September 2023 (APNIC republication). It is now exposed by Cloudflare Bot Management, AWS CloudFront and WAF, Google Cloud Armor, Fastly, Akamai and others listed in the FoxIO README.
For scrapers this cuts both ways. Randomising your extension order no longer changes your fingerprint, so that trick is dead. What still matters is the set of ciphers, the set of extensions and the order of signature algorithms. In practice that means using the TLS stack a real browser uses, or a library that copies one closely. If you need the original-order view (for example to compare against JA3), the spec defines JA4_o and the raw variants JA4_r / JA4_ro. BrowserLeaks returns all four. Our TLS fingerprinting explainer covers JA3 in more depth.
The JA4+ family: JA4S, JA4H, JA4X, JA4T
JA4 is one method in a larger suite. The others look at the server, the HTTP layer, certificates and TCP, and they come under a different licence.
| Method | Fingerprints | Format, briefly |
|---|---|---|
| JA4S | Server's ServerHello | protocol + version + extension count + ALPN, then the chosen cipher, then a hash of the server's extensions |
| JA4H | HTTP request | method (ge, po…), version (11, 20), cookie c/n, referer r/n, header count, first 4 chars of Accept-Language; then hashes of header names in order, sorted cookie names, and sorted cookie name=value pairs |
| JA4X | X.509 certificate | hashes of issuer RDN OIDs, subject RDN OIDs and extension OIDs (how the cert was built, not its values) |
| JA4T | TCP SYN | window size, TCP options in order, MSS, window scale, e.g. 64240_2-1-3-1-1-4_1460_8 for Windows 11 |
Formats are taken from FoxIO's technical details diagrams and the reference Python implementation. JA4H matters most for scraping. Its b section hashes header names in the order sent, so a client that sends Chrome's headers in the wrong order is visible even if every value is right. Our HTTP/2 fingerprinting post covers the related SETTINGS and pseudo-header order signals. JA4T exposes the OS: a Linux server's SYN does not look like Windows, whatever the User-Agent says.
Licensing
According to the repository's licensing section, JA4 (the TLS client fingerprint) is BSD 3-Clause, like JA3, and FoxIO says it has no patent claims on it. JA4S, JA4L, JA4LS, JA4H, JA4X, JA4SSH, JA4T, JA4TS, JA4TScan, JA4D, JA4D6 and the other "JA4+" methods are patent-pending and licensed under the FoxIO License 1.1. That licence allows internal and academic use, but a vendor selling JA4+ fingerprinting as part of a product needs an OEM licence.
QUIC and HTTP/3: what the q prefix tells you
A JA4 starting with q was computed from a QUIC Initial packet, meaning the client was setting up HTTP/3. QUIC carries a TLS 1.3 ClientHello inside its CRYPTO frames. The Initial packet's protection keys are derived from the Destination Connection ID the client chooses (RFC 9001 §5.2), so any on-path observer or edge server can decrypt it and fingerprint the ClientHello exactly as it would over TCP.
Chrome's QUIC fingerprint looks very different from its TCP one: q13d0312h3_55b375c5d22e_178839b6cec1 in FoxIO's example. That is 3 ciphers (TLS 1.3 only, since QUIC forbids older versions) and ALPN h3. This matters for automation because browsers move to HTTP/3 once a site advertises it, while most HTTP clients never do. Cloudflare's documented JA4 Signals include an h2h3_ratio_1h value: the share of traffic for a given JA4 arriving over HTTP/2 versus HTTP/3. A "Chrome" TLS fingerprint that never shows up over QUIC on an HTTP/3 site is a statistical outlier. The curl_cffi README states that HTTP/3 fingerprint impersonation has been supported since v0.15.0.
How to see your own JA4
The fastest check is an echo service that reads your ClientHello and returns the fingerprint. These three returned JA4 in our tests, and they agreed with each other byte for byte:
- tls.peet.ws/api/all: JSON with
tls.ja4,tls.ja4_r, JA3, the HTTP/2 Akamai fingerprint andhttp_version. - tls.browserleaks.com/json: JSON with
ja4,ja4_r,ja4_o,ja4_ro. - Scrapfly's JA3/JA4 tool, whose API at
tools.scrapfly.io/api/fp/ja3returnsja4andja4_r.
curl -s https://tls.peet.ws/api/all | python3 -c \
"import sys, json; d = json.load(sys.stdin); print(d['http_version'], d['tls']['ja4'])"
Run the same check through your proxy, not just from your laptop. A forward proxy that tunnels with CONNECT passes your ClientHello through untouched, but a TLS-intercepting proxy, corporate middlebox or some SDK layers will substitute their own. For example, with a ProxyHat residential exit:
curl -s -x http://USERNAME:PASSWORD@gate.proxyhat.com:8080 https://tls.peet.ws/api/all
Wireshark and tshark
Wireshark has computed JA4 natively since 4.2.0, in the display-filter field tls.handshake.ja4 (and tls.handshake.ja4_r), per the TLS field reference. FoxIO's JA4+ plugin (Wireshark 4.4.0 or later) adds the rest of the suite under ja4.*, such as ja4.ja4s, ja4.ja4h and ja4.ja4t. To pull fingerprints out of a capture:
tshark -r capture.pcapng -Y "tls.handshake.ja4" -T fields \
-e ip.dst -e tls.handshake.extensions_server_name -e tls.handshake.ja4
Capture on a machine where you control the client. To fingerprint HTTPS traffic from a headless browser, run it locally and capture on the loopback or outbound interface. The ClientHello is unencrypted, so you do not need TLS keys for JA4. You do need them for JA4H on HTTPS.
What this means in practice: headers must match the handshake
Changing the User-Agent does not change your JA4. A Python script that sends a Chrome User-Agent still presents t13d3112h1_e8f1e7e78f70_b26ce05bbdd6 in the handshake, before a single header is read. That mismatch (claims Chrome, negotiates like OpenSSL, asks for HTTP/1.1) is one of the cheapest checks an edge can run. It is also exactly what JA4 databases are built to catch.
There are two mainstream fixes. Both have limits.
- curl_cffi (Python bindings to curl-impersonate) builds the ClientHello and HTTP/2 frames to match a named browser profile:
requests.get(url, impersonate="chrome"). In our captures its current Chrome profiles produced the same JA4 as a real headless Chrome 148. Our curl_cffi guide covers sessions and proxies. - uTLS for Go replaces
crypto/tls's ClientHello with a "parrot" such astls.HelloChrome_Autothroughtls.UClient(conn, &config, helloID). Its README is explicit that parroting can be imperfect and does not extend beyond the ClientHello. Our Go uTLS walkthrough shows how to wire it intonet/http.
What impersonation does not solve
A matching JA4 only gets you past the first filter.
- The JA4 must agree with the claimed browser version: a Chrome 148 User-Agent with a pre-switch ALPS codepoint (
b0da82dd1658) is its own small mismatch. - JA4H header order must match too.
- JA4T and IP data reveal the host OS and network.
- JavaScript challenges never run in an HTTP client.
- Behaviour (request rate, navigation pattern) is judged separately.
Pick the profile that matches the User-Agent you send, keep sessions alive so resumption looks normal, and keep the exit IP plausible for the traffic. A residential exit fixes the network-reputation layer, not the TLS layer. Check the fingerprint through the proxy, as shown above.
Finally, the reason to understand fingerprints is to make legitimate automation honest and predictable, not to get past a site that has decided to refuse automated access. Respect robots.txt and terms of service where they apply, and remember that scraping personal data brings legal obligations a perfect JA4 does nothing about.






