Explainer
TLS Fingerprints, JA3 and JA4: Why Cloudflare Challenges curl
How JA3 and JA4 fingerprint a TLS handshake, what Cloudflare shows Bot Management customers, and why Python with Chrome's user agent still stands out.
By ZeroCaptcha Engineering5 min readPublished
Before your client sends a single HTTP header, its TLS handshake has already described it. The
ClientHello message lists the TLS versions, cipher suites and extensions your TLS library
supports, in its own order, and JA3 and JA4 turn that list into a fingerprint. Cloudflare
exposes both to Enterprise customers with Bot Management (cf.bot_management.ja3_hash and
cf.bot_management.ja4), for rules and analytics. That is why curl or Python requests sending
Chrome’s User-Agent still does not look like Chrome: the header says one thing, and the handshake
says another.
This explainer covers how the two fingerprints are built, why JA4 replaced JA3 for browsers, what Cloudflare does with them, and what a legitimate automated client can do about it, as documented on 1 October 2026.
JA3: the first TLS fingerprint
JA3 was published by Salesforce. It takes five fields of the ClientHello, in this order:
SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat, writes each field’s
values as decimal numbers joined by hyphens, joins the fields with commas, and takes the MD5 hash.
The project’s own example:
769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0--> ada70206e40642a3e4461f35503241d5GREASE values, random placeholders some clients add, are ignored “to ensure that programs utilizing GREASE can still be identified with a single JA3 hash”. The Salesforce repository was archived on 1 May 2025 and is read-only.
Why browsers broke JA3
JA3 keeps the extensions in the order the client sent them. In November 2022 the Chrome team announced it would “Randomize the order of TLS ClientHello extensions, to reduce potential ecosystem brittleness”, so that servers and middleboxes could not come to depend on one fixed order. A browser that shuffles its extensions on every connection has a different JA3 on every connection, which makes JA3 useless for recognising it.
JA4: sorted, readable, and stable
JA4, from FoxIO, fixes that by sorting. A JA4 fingerprint has three parts, a_b_c, such as the
specification’s example t13d1516h2_8daaf6152771_e5627efa2ab1:
| Part | Example | What it says |
|---|---|---|
| a | t13d1516h2 |
t TLS over TCP (q is QUIC, d DTLS); 13 TLS 1.3; d a server name was sent (i if not); 15 ciphers and 16 extensions, GREASE excluded; h2, the first and last characters of the first ALPN value |
| b | 8daaf6152771 |
“A 12 character truncated sha256 hash of the list of ciphers sorted in hex order” |
| c | e5627efa2ab1 |
A 12-character truncated SHA-256 of the extensions sorted by hex value (without the server name and ALPN extensions), then the signature algorithms in their original order |
Cloudflare puts the benefit plainly: “JA4 improves on JA3 by sorting ClientHello extensions, which reduces the number of unique fingerprints for modern browsers and makes grouping easier.” The first part is also readable, so a person can see at a glance that a client offered HTTP/2 over TLS 1.3 with a server name.
What Cloudflare does with them
| Field or feature | What it is | Who has it |
|---|---|---|
cf.bot_management.ja3_hash |
The request’s JA3 | Enterprise customers who bought Bot Management |
cf.bot_management.ja4 |
The request’s JA4 | Enterprise customers who bought Bot Management |
| JA4 Signals | “Aggregate intelligence data for each JA4 fingerprint based on traffic across the Cloudflare network”, over the last hour | Bot Management customers, in Security Analytics and Security Events |
The fingerprints can be used in WAF custom rules, Transform Rules and Workers, and appear in Bot Analytics, Security Events, the GraphQL API and Logpush. Cloudflare’s examples are blocking malicious traffic with custom rules, allowing legitimate traffic that was flagged by mistake, and recognising a mobile app by its consistent fingerprint across devices. JA4 Signals cannot be used as filters in WAF custom rule expressions.
Some JA4 Signals fields show how a fingerprint gets judged across the network:
browser_ratio_1h: the proportion of that fingerprint’s requests that carry a browser user agent;h2h3_ratio_1h: the proportion that used HTTP/2 or HTTP/3;uas_rank_1h: how many different user agents the fingerprint was seen with (lower means more);ips_rank_1h: how many distinct client IP addresses sent it (lower means more).
Cloudflare does not publish how its bot score weighs these. But a Python library’s fingerprint sent with a Chrome user agent is exactly the kind of mismatch such statistics make visible.
Fingerprints are absent on plain HTTP, on some Worker subrequests, when Bot Management is skipped for a request, and on connections that resume an earlier TLS session.
See your own client’s fingerprint
curl_cffi, a Python binding for curl-impersonate, describes itself as “A http client that can
impersonate browser tls/ja3/http2 fingerprints”. Its README uses a public echo service to show the
difference. This compares Python requests with curl_cffi impersonating Chrome, printing only
the fingerprint fields the service returns:
import curl_cffiimport requests
URL = "https://tls.browserleaks.com/json"
def fingerprint_fields(result): return {key: value for key, value in result.items() if "ja" in key.lower()}
plain = requests.get(URL, timeout=30).json()as_chrome = curl_cffi.get(URL, impersonate="chrome", timeout=30).json()print("requests:", fingerprint_fields(plain))print("curl_cffi as Chrome:", fingerprint_fields(as_chrome))The two differ, although both are “Python”. Only the second matches what a Chrome browser sends.
What to do in an automated client
- Match the handshake to the user agent. If you send a Chrome
User-Agent, use a client that impersonates Chrome’s TLS and HTTP/2 fingerprints, such ascurl_cffior curl-impersonate, or a real browser. If you can’t, send your library’s honest user agent instead of a borrowed one. - Keep the pair with the cookie. A
cf_clearancecookie is tied to the visitor and device that earned it. Send it with the exact user agent it was issued for, from the same IP address, with a client whose handshake fits that user agent. The cf_clearance cookie explained has the details. - Remember what no fingerprint fixes. A block (error 1020) or a ban is the site owner’s decision, not a detection mistake. See Cloudflare 403 Forbidden to tell which one you have.
Using a cf_clearance cookie from curl_cffi, through the proxy that earned it:
import os
import curl_cffi
proxy = os.environ["PROXY_URL"] # the proxy the clearance was earned throughresponse = curl_cffi.get( "https://shop.example.com/", impersonate="chrome", proxies={"http": proxy, "https": proxy}, headers={"User-Agent": os.environ["CLEARANCE_USER_AGENT"]}, cookies={"cf_clearance": os.environ["CF_CLEARANCE"]}, timeout=30,)print(response.status_code, response.headers.get("cf-mitigated"))Pick an impersonation target whose browser version matches the user agent you send, where your
curl_cffi version offers one. ZeroCaptcha’s challenge task returns the cf_clearance cookie with the user agent it was issued for, earned through your own proxy, for exactly this use.
See Cloudflare WAF and 5-second challenges and the
Cloudflare WAF and 5-second challenge solver.
Sources
- Cloudflare bots: JA3/JA4 fingerprint and JA4 Signals (checked 1 October 2026).
- FoxIO: JA4 technical details (checked 1 October 2026).
- Salesforce: JA3 (checked 1 October 2026).
- Chromium blink-dev: Intent to Ship: TLS ClientHello extension permutation, 18 November 2022 (checked 1 October 2026).
- curl_cffi (checked 1 October 2026).
- Cloudflare challenges: clearance (checked 1 October 2026).
The team that builds and runs the ZeroCaptcha API. Articles are drafted with AI tools, then checked against the API's code and the primary sources each one cites.