Anti-Bot

What Is TLS Fingerprinting (JA3/JA4)?

What Is TLS Fingerprinting (JA3/JA4)? — conceptual illustration
On this page

TLS fingerprinting is a way to recognize what software made a connection just by looking at how it sets up encryption — before the server reads a single byte of your request. TLS is the encryption layer behind https, and every connection starts with a "handshake" where the client announces which ciphers, extensions, elliptic curves, and ALPN values it supports. The exact list and order are turned into a short fixed ID (a hash): JA3 since 2017, JA4+ since 2023. Anti-bot vendors run this check at the CDN edge (the network layer in front of a site) and compare your hash to a catalogue of every popular HTTP library — so a scraper using Python requests can be flagged before its headers even arrive.

Quick facts

StandardJA4+ (2023, FoxIO) — replaces JA3 (2017, Salesforce)
Computed atCDN edge (Cloudflare Rust crate, Akamai EdgeWorker)
Fields hashedTLS version, cipher suites, extensions, curves, ALPN
Flags clients likerequests, httpx, urllib3, curl (default), Go net/http
Matched bycurl_cffi, tls-client, real browsers, Camoufox

How TLS fingerprinting works

The first message of a TLS handshake is the Client Hello. It lists, in a fixed order, the protocol version, the cipher suites the client supports, named curves, point formats, signature algorithms, and the TLS extensions it sends. The catch: each software library fills in this list differently, so the combination acts like a signature for the tool that built it. Python requests looks nothing like Chrome; Go net/http looks nothing like Firefox; curl looks like none of them. Anti-bot vendors hash these fields and compare the result to known browser baselines.

This hash is computed at the CDN edge, before any HTTP byte is parsed, before any HTML is served, before any JavaScript runs. That is why a perfect User-Agent spoof from requests still gets blocked — the TLS layer already gave the game away.

JA3 vs JA4 — what changed in 2023

The first widely adopted fingerprint was JA3 (Salesforce, 2017): an MD5 hash (a short fixed-length code) of SSLVersion,Cipher,Extensions,EllipticCurve,EllipticCurvePointFormat. It worked for years but had two problems — TLS 1.3 added GREASE values (random padding browsers insert on purpose), which made the hash change run to run, and a single MD5 hid which field had actually changed.

JA4+ (FoxIO, 2023) is the replacement. Instead of one MD5 it produces a structured fingerprint with separate parts: the TLS Client Hello (JA4), the HTTP/2 client preface and SETTINGS frame (JA4H), the TLS server response (JA4S), and TCP-level timings (JA4T). Each part can be read on its own, ignores GREASE so it stays stable, and is human-readable rather than one opaque hash.

FingerprintWhat it coversStatus
JA3TLS Client Hello (MD5)Legacy — still seen on older WAFs
JA4TLS Client Hello, GREASE-stableCurrent standard at major vendors
JA4HHTTP/2 client preface + SETTINGS frame orderRequired to pass Akamai, Cloudflare
JA4SServer-side TLS response fingerprintUsed by vendors to detect MITM proxies
JA4TTCP options + window size + timingRare — DataDome experimental

If you are still targeting JA3-only on a 2026 site, your fingerprint can look correct in the testing tool tls.peet.ws yet still block in production. Match JA4 + JA4H together.

HTTP/2 framing — the layer underneath TLS

Even if your TLS handshake matches Chrome exactly, the HTTP/2 connection that runs on top of it has its own fingerprint. When Chrome opens an HTTP/2 connection it sends a specific SETTINGS frame — the opening message that declares connection parameters (HEADER_TABLE_SIZE=65536, INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144) — followed by a WINDOW_UPDATE frame with a delta of 15663105. The frame order and exact values change with each Chrome major version.

Go's net/http2 and Python's httpx use different SETTINGS values and a different frame order. Vendors hash that SETTINGS frame and compare. curl_cffi ships with the Chrome-matching values baked in; tls-client and noble-tls need them set by hand. This is the second-most-common reason a request with a correct JA4 still gets a 403.

QUIC and HTTP/3 — the next surface

Cloudflare and Google now serve a large share of traffic over QUIC, which is HTTP/3 running on UDP instead of TCP. QUIC has its own handshake, its own SETTINGS-frame equivalent, and therefore its own fingerprint surface — and almost no scraping library supports it yet. Most scrapers quietly fall back to HTTP/2 by sending an Alt-Svc-incompatible header set, and that fallback is itself a signal.

For now QUIC-aware fingerprinting rarely shows up in production blocks, but expect it to matter by late 2026, especially on Google properties and Cloudflare's premium tier. The fix is the same as everywhere else: pick a library that completes the handshake the way Chrome does, end to end.

Why this matters for scraping

Say your scraper sends a User-Agent header claiming to be Chrome 131 on macOS, but its JA4 hash matches Python urllib3. You now flag faster than if you had spoofed nothing at all — because the mismatch between what you claim and what your TLS shows is the signal. Spoofing headers without also spoofing TLS is the single most common rookie mistake in modern scraping.

JA4+ also covers JA4H (HTTP header order and content), JA4X (X.509 certificates, the certificates that prove identity in TLS), and JA4T (TCP options + window). The profile an anti-bot builds reaches well beyond the cipher list.

How clients match a browser's TLS

To present a TLS handshake consistent with a real browser, scrapers use an HTTP client that reproduces that browser's TLS. curl_cffi wraps curl-impersonate, a patched build of curl that uses BoringSSL (Chrome's own TLS library) with Chrome's exact cipher list and HTTP/2 SETTINGS frames. A single impersonate="chrome131" argument gives you a JA4 that is indistinguishable from real Chrome. tls-client (Go), noble-tls, and hrequests do the same in their own ecosystems. Real browsers — Chrome, Firefox, Camoufox — speak real TLS by definition, so they pass without any tricks.

Keep your impersonation profiles current. A Chrome 120 fingerprint in 2026 is itself suspicious, because real users have long since updated.

Code example

python
from curl_cffi import requests

# One argument replaces the entire TLS stack with Chrome's
r = requests.get(
    "https://protected-site.com/api",
    impersonate="chrome131",
    proxies={"https": "http://user:pass@residential-proxy:port"},
    timeout=20,
)
print(r.status_code, len(r.text))

Related terms

What Is Anti-Bot Detection?
Anti-bot detection is the set of techniques websites use to tell automated traffic apart from real human visitors — and then block, challeng…
What Is Browser Fingerprinting?
Browser fingerprinting is a technique that identifies and tracks a visitor by combining dozens of small, observable characteristics of their…
What Is curl_cffi?
curl_cffi is a Python HTTP client whose TLS fingerprint looks exactly like real Chrome, Firefox, or Safari. TLS is the encryption layer behi…
What Is Cloudflare Turnstile?
Cloudflare Turnstile is a service that checks whether a visitor is a real human, but without showing the kind of puzzle a normal CAPTCHA doe…
What Is HTTP/2 Fingerprinting?
HTTP/2 fingerprinting identifies an HTTP client from its SETTINGS frame and frame-level behaviour, independent of the TLS layer. Think of it…
Anti-Bot Vendor Detection Cheatsheet
A useful first step when working with any protected site you are authorized to access is identifying which anti-bot vendor sits in front of …
What Is the Scrapy + Go TLS Sidecar Architecture?
The Scrapy + Go TLS sidecar architecture is the most common production pattern for scraping Akamai- and Cloudflare-protected sites at scale.…
What Is JA4 Fingerprinting?
JA4 is a way to identify a browser by the fingerprint of its TLS handshake — TLS being the encryption layer behind https. It replaced the ol…
How Does Deobfuscation Work?
Deobfuscation is the process of turning deliberately unreadable code back into something a human can read and reason about. Obfuscators scra…
What Is JA3 Fingerprinting?
JA3 is a method for fingerprinting a TLS client by hashing the fields of its Client Hello. TLS is the encryption layer behind https, and the…
What Is Cloudflare Error 1020 (Access Denied)?
Cloudflare Error 1020 "Access Denied" means a Cloudflare firewall (WAF) rule on the site has blocked your request outright. Unlike Error 101…
How to Make curl Ignore SSL Certificate Errors
To make curl ignore SSL certificate errors, add the -k (or --insecure) flag: curl -k https://example.com. This tells curl to skip certificat…
Web Scraping With curl: A Complete 2026 Guide
Web scraping with curl means fetching pages directly from the command line, setting headers, cookies, and proxies with curl's flags, then pi…
What Is a User Agent?
A user agent is a short text string a client sends in the User-Agent HTTP header to tell a server what software is making the request. Every…

Concept map

How TLS Fingerprinting (JA3/JA4) connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

What is the difference between JA3 and JA4?

JA3 (2017) hashed the TLS extensions in the exact order they were sent. Chrome started shuffling that order in 2022, which broke JA3 — the same browser produced different hashes across sessions. JA4 (2023) sorts the extensions alphabetically and removes GREASE (deliberate random padding) before hashing, so the fingerprint stays stable no matter how the order is randomised. JA4+ extends the same idea to HTTP headers, certificates, and TCP options.

Can I just spoof my User-Agent to fix this?

No — and it usually makes detection faster, not slower. The User-Agent header is read only after the TLS handshake. If your User-Agent claims Chrome but your TLS hash matches Python urllib3, that mismatch is itself a strong bot signal. You have to spoof the TLS layer as well.

Does using HTTPS hide the fingerprint?

No — the TLS Client Hello is sent unencrypted at the very start of every HTTPS connection. That handshake is the thing being fingerprinted, and it happens before any encrypted data flows.

Which Python library should I use?

curl_cffi is the most popular drop-in replacement for requests. It supports Chrome, Firefox, and Safari impersonation profiles and handles HTTP/2 SETTINGS frames correctly. It is the default first-step upgrade for any Python scraper running into a real anti-bot.

Is JA3 still useful, or do I have to target JA4 now?

For older WAFs (web application firewalls) such as Imperva or AWS WAF Bot Control on default settings, JA3 alone is still enough to score the request. But for Akamai, Cloudflare Bot Management, DataDome, and PerimeterX in 2026, JA4 and JA4H are scored together — matching only JA3 produces a "wrong-shape Chrome" signal that gets ranked as a bot.

Do I need to handshake over QUIC to handle modern anti-bot TLS checks today?

Not yet. Cloudflare still accepts HTTP/2 from real Chrome on the vast majority of sites, and most scraping libraries don't support QUIC anyway. But the gap between "scrapers use HTTP/2" and "real users use HTTP/3" is starting to become a signal on Cloudflare's premium tier — worth keeping an eye on.

Last updated: 2026-05-31