# 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.

- Source: https://zerocaptcha.io/blog/tls-fingerprint-ja4-cloudflare
- Published: 2026-10-01
- Author: ZeroCaptcha Engineering

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:

```text
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:

```python
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](https://zerocaptcha.io/guides/cf-clearance-cookie-explained) has the details.
- **Remember what no fingerprint fixes.** A block (error [1020](https://zerocaptcha.io/blog/cloudflare-error-1020)) or a
  ban is the site owner's decision, not a detection mistake. See
  [Cloudflare 403 Forbidden](https://zerocaptcha.io/blog/cloudflare-403-forbidden) to tell which one you have.

Using a `cf_clearance` cookie from `curl_cffi`, through the proxy that earned it:

```python
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](https://zerocaptcha.io/docs/challenges) and the
[Cloudflare WAF and 5-second challenge solver](https://zerocaptcha.io/cloudflare-challenge-solver).

## Sources

- [Cloudflare bots: JA3/JA4 fingerprint](https://developers.cloudflare.com/bots/additional-configurations/ja3-ja4-fingerprint/)
  and [JA4 Signals](https://developers.cloudflare.com/bots/additional-configurations/ja3-ja4-fingerprint/signals-intelligence/)
  (checked 1 October 2026).
- [FoxIO: JA4 technical details](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md)
  (checked 1 October 2026).
- [Salesforce: JA3](https://github.com/salesforce/ja3) (checked 1 October 2026).
- [Chromium blink-dev: Intent to Ship: TLS ClientHello extension permutation](https://groups.google.com/a/chromium.org/g/blink-dev/c/bYZK81WxYBo),
  18 November 2022 (checked 1 October 2026).
- [curl_cffi](https://github.com/lexiforest/curl_cffi) (checked 1 October 2026).
- [Cloudflare challenges: clearance](https://developers.cloudflare.com/cloudflare-challenges/concepts/clearance/)
  (checked 1 October 2026).

## 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.
