Skip to content

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 5 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
--> ada70206e40642a3e4461f35503241d5

GREASE 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_cffi
import 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 as curl_cffi or 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_clearance cookie 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 through
response = 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

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.

Questions

What is a JA4 fingerprint?

It is a short string that describes how a client starts a TLS connection: protocol, TLS version, whether it sent a server name, how many ciphers and extensions it offered, its first ALPN value, and truncated SHA-256 hashes of its sorted ciphers and extensions. The same client software gives the same JA4.

Does Cloudflare use JA3 and JA4 fingerprints?

Yes. Cloudflare exposes cf.bot_management.ja3_hash and cf.bot_management.ja4 to Enterprise customers who bought Bot Management, for WAF rules, Workers and analytics, and publishes JA4 Signals, hourly statistics about each fingerprint across its network.

Why does curl get a Cloudflare challenge when a browser does not?

curl runs no JavaScript, so it cannot pass a challenge, and its TLS handshake looks like curl's TLS library, not like a browser, whatever User-Agent header it sends. A client that impersonates the browser's handshake, or a real browser, looks consistent.

Read next

This article is part of the Cloudflare WAF and 5-second challenge solver hub. Every task is charged only when a token is ready.

Get an API key