Your scraper sends perfect Chrome headers, runs behind a clean residential IP, and still gets a 403 on the very first request. No captcha, no challenge page, nothing to solve. The site identified you before a single byte of HTTP was exchanged — during the TLS handshake.
This article explains how JA3 and JA4 fingerprinting actually work, why they're the quiet workhorse of every major anti-bot system in 2026, and how to see your own fingerprint in ten seconds. If you want the bypass tooling first, we covered that in Bypass TLS Fingerprinting with curl_cffi & tls-client — this is the companion piece on how the detection side works.
Before the theory, look at your own handshake. Open our free JA3/JA4 checker — it echoes back the TLS fingerprint your client just presented and tells you what it matches:
# From a terminal - see what curl's TLS looks like:
curl https://check.jibaoproxy.com/api/fingerprint
# Then open the same URL in Chrome and compare.
Run both and you'll see the point of this entire article: curl and Chrome produce completely different fingerprints, no matter what User-Agent header you set.
Every HTTPS connection starts with a ClientHello message, sent in cleartext before any encryption begins. In it, your client announces:
Here's the detection insight: these lists and their exact order are baked into the TLS library your client was compiled against. Chrome's BoringSSL announces one specific combination. Python's OpenSSL announces a different one. Go's crypto/tls a third. You cannot change this with headers — it's negotiated below the HTTP layer, by the library itself.
JA3 (Salesforce, 2017) concatenates five ClientHello fields — TLS version, ciphers, extensions, curves, point formats — into a string and MD5-hashes it:
771,4865-4866-4867-49195-49199,0-23-65281-10-11-35-16,29-23-24,0
|
v MD5
cd08e31494f9531f560d64c695473da9
One hash per TLS stack. Every copy of Chrome 137 on Windows produces (nearly) the same JA3; every python-requests on OpenSSL 3.x produces the same different JA3. Defenders maintain lookup tables: hash X = Chrome, hash Y = requests, hash Z = Go bot. If your User-Agent says Chrome but your JA3 says OpenSSL, you're blocked — instantly, silently, cheaply.
JA3 had problems: Chrome started randomizing extension order in 2023 (breaking naive JA3), and MD5 is unstructured — one hash tells you nothing about why two clients differ. JA4 (FoxIO, 2023) fixed both and is what serious vendors run today:
JA4 = t13d1516h2_8daaf6152771_b0da82dd1658
| | |
a: protocol b: ciphers c: extensions
(TLS 1.3, 16 ciphers, (sorted, truncated
h2 ALPN, domain SNI) SHA256)
Every system that matters: Cloudflare exposes JA3/JA4 directly in its bot-management rules; DataDome and PerimeterX feed it into their network-layer scoring (see our DataDome/PerimeterX guide); Akamai has run TLS fingerprinting the longest of anyone. It's popular because it's free to compute, impossible to fake from the HTTP layer, and catches 90% of naive automation — every requests/httpx/axios/Go-http script ever written — before any JavaScript needs to run.
This surprises people: a proxy changes your IP, but your TLS handshake passes through unchanged. The CONNECT tunnel (or SOCKS5 stream) carries your ClientHello bytes verbatim to the target. Residential IP + requests-library JA3 = "a bot running on a residential IP." Better than a datacenter IP, still a bot.
The IP and the TLS fingerprint are independent signals and you need both clean:
| IP | TLS fingerprint | Verdict in 2026 |
|---|---|---|
| Datacenter | Library (requests/Go) | Blocked everywhere that cares |
| Residential | Library | Blocked on JA4-checking sites |
| Datacenter | Browser-impersonated | Blocked on IP reputation |
| Residential | Browser-impersonated | Passes network-layer checks |
Two paths, by tooling:
HTTP clients — use an impersonation library that re-implements a browser's exact ClientHello: curl_cffi (Python), tls-client (Python/Go), got-scraping (Node). Full working code in the curl_cffi guide:
from curl_cffi import requests
r = requests.get(
"https://target.example",
impersonate="chrome",
proxies={"https": "socks5h://USERNAME:[email protected]:913"},
)
Real browsers (Playwright, browser agents, anti-detect browsers) — nothing to do at the TLS layer; the fingerprint is genuinely Chrome's. Your risk lives elsewhere: IP reputation and CDP detection.
Then verify, don't assume. Versions drift — an impersonation library pinned to Chrome 120's handshake reads as "suspiciously outdated browser" in 2026. Re-test after every dependency update:
Pair your impersonated TLS with residential IPs — $5 free credit, no card required.
Start Free TrialNew users get $5 USDT instantly, plus an extra first-deposit reward — limited-time offer.