TLS/JA3 Fingerprinting Explained for Web Scrapers
TLS fingerprinting is one of the most effective methods websites use to detect automated scrapers — and one of the least understood. Unlike browser-level detection that requires JavaScript, TLS fingerprinting happens during the initial connection handshake, before any page content is delivered. This means it catches bots that have perfect headers, cookies, and JavaScript execution.
What Is TLS Fingerprinting?
Every time your client connects to an HTTPS website, it performs a TLS handshake. During this handshake, the client sends a “Client Hello” message that contains:
- TLS version supported
- Cipher suites the client can use (and their order)
- Extensions supported (and their order)
- Elliptic curves supported
- Compression methods
- Signature algorithms
Different HTTP clients send different Client Hello messages. Chrome’s looks different from Firefox’s, which looks different from Python’s requests library. This difference creates a unique fingerprint.
What Is JA3?
JA3 is a method for hashing TLS Client Hello parameters into a single, comparable hash. Created by Salesforce researchers, it takes five fields from the Client Hello and generates an MD5 hash:
JA3 = MD5(TLSVersion, Ciphers, Extensions, EllipticCurves, EllipticCurvePointFormats)Example JA3 Hashes
| Client | JA3 Hash |
|---|---|
| Chrome 122 (Windows) | cd08e31494f9531f560d64c695473da9 |
| Firefox 123 (Windows) | b32309a26951912be7dba376398abc3b |
| Python requests 2.31 | 56c42b50281e63b53b4e154f6c0e8e0f |
| Go net/http | 473cd7cb9faa642487833f5dab16ab1d |
| curl (default) | 456523fc94726331a8d59059f21d4390 |
Notice how each client has a unique hash. When Cloudflare or another anti-bot system sees the JA3 hash for Python requests paired with a Chrome user agent header, it knows the request is fake.
How Anti-Bot Systems Use TLS Fingerprinting
1. Client Identification
The primary use: identify what software is actually making the request, regardless of what the HTTP headers claim.
Scenario: Scraper sends User-Agent: Chrome/122
TLS fingerprint: Python requests 2.31
Result: MISMATCH → Block or challenge2. Known Bot Signatures
Anti-bot databases maintain lists of JA3 hashes associated with known scraping tools:
- Python
requests,httpx,aiohttp - Go
net/http,colly - Node.js
node-fetch,axios - Java
HttpClient,OkHttp - curl (default builds)
Any request with these fingerprints gets flagged immediately.
3. JA3S (Server Response Fingerprinting)
JA3S fingerprints the server’s TLS response. Anti-bot systems can use JA3S to detect when clients negotiate unusual cipher suites or TLS parameters that real browsers wouldn’t accept.
JA3 in Detail
Breaking Down a Client Hello
Client Hello:
TLS Version: 0x0303 (TLS 1.2)
Cipher Suites (17):
TLS_AES_128_GCM_SHA256 (0x1301)
TLS_AES_256_GCM_SHA384 (0x1302)
TLS_CHACHA20_POLY1305_SHA256 (0x1303)
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
...
Extensions (16):
server_name (0x0000)
status_request (0x0005)
supported_groups (0x000a)
ec_point_formats (0x000b)
signature_algorithms (0x000d)
...
Elliptic Curves (3):
x25519 (0x001d)
secp256r1 (0x0017)
secp384r1 (0x0018)The JA3 string concatenates: 771,4865-4866-4867-49195-49199-...,0-5-10-11-13-...,29-23-24,0
Then hashes it: MD5("771,4865-4866-4867-...") = cd08e31494f9531f560d64c695473da9
How Python Requests Differs from Chrome
Python’s requests library uses urllib3 with Python’s ssl module. The key differences:
- Cipher suite order — Python offers ciphers in a different order than Chrome
- Extensions — Python includes different TLS extensions
- GREASE values — Chrome includes random GREASE values (0x0a0a, 0x1a1a, etc.) that Python doesn’t
- ALPS — Chrome supports Application-Layer Protocol Settings; Python doesn’t
- Encrypted Client Hello — Chrome supports ECH; most Python HTTP libraries don’t
How to Spoof TLS Fingerprints
Method 1: curl_cffi (Python — Recommended)
curl_cffi uses curl-impersonate to produce browser-identical TLS fingerprints:
from curl_cffi import requests
# Impersonate Chrome's TLS fingerprint
response = requests.get(
"https://tls.peet.ws/api/all",
impersonate="chrome120"
)
# Check the JA3 hash
data = response.json()
print(f"JA3: {data['tls']['ja3_hash']}")
print(f"JA3 matches Chrome: True")Supported Impersonation Targets
from curl_cffi import requests
# Available browser impersonations
targets = [
"chrome99", "chrome100", "chrome101",
"chrome104", "chrome107", "chrome110",
"chrome116", "chrome119", "chrome120",
"safari15_3", "safari15_5", "safari17_0",
"edge99", "edge101",
]
# Session with persistent fingerprint
session = requests.Session(impersonate="chrome120")
session.proxies = {
"http": "http://user:pass@residential.example.com:7777",
"https": "http://user:pass@residential.example.com:7777"
}
response = session.get("https://target-site.com")Method 2: tls-client (Python)
import tls_client
session = tls_client.Session(
client_identifier="chrome_120",
random_tls_extension_order=True
)
response = session.get("https://target-site.com", headers={
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
)
})Method 3: Real Browser (Most Reliable)
Using a real browser through automation produces authentic TLS fingerprints:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# Chromium produces Chrome's real TLS fingerprint
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://tls.peet.ws/api/all")
# TLS fingerprint will match real Chrome
browser.close()Method 4: curl-impersonate (Command Line)
# curl-impersonate compiles curl with browser-specific TLS settings
curl-impersonate-chrome https://tls.peet.ws/api/all
# Or with specific version
curl_chrome120 https://target-site.comJA4 and Beyond
JA3 has known limitations (hash collisions, inability to distinguish minor version differences). Newer fingerprinting methods are emerging:
JA4 Fingerprint
JA4 improves on JA3 by:
- Separating TLS version, cipher count, and extension count into readable components
- Including ALPN and SNI information
- Using a more collision-resistant hash structure
JA4 format: t13d1516h2_8daaf6152771_e5627efa2ab1
| | | | |
| | | | Extension hash
| | | Cipher hash
| | ALPN
| Cipher count + Extension count
TLS versionHTTP/2 Fingerprinting
Beyond TLS, HTTP/2 settings (SETTINGS frame, WINDOW_UPDATE, PRIORITY frames) also vary between clients and create additional fingerprinting vectors.
Chrome HTTP/2 SETTINGS:
HEADER_TABLE_SIZE: 65536
MAX_CONCURRENT_STREAMS: 1000
INITIAL_WINDOW_SIZE: 6291456
MAX_HEADER_LIST_SIZE: 262144
Python httpx HTTP/2 SETTINGS:
HEADER_TABLE_SIZE: 4096
MAX_CONCURRENT_STREAMS: 100
INITIAL_WINDOW_SIZE: 65535Testing Your TLS Fingerprint
from curl_cffi import requests
# Check your fingerprint against known browser fingerprints
session = requests.Session(impersonate="chrome120")
resp = session.get("https://tls.peet.ws/api/all")
data = resp.json()
print(f"TLS Version: {data['tls']['version']}")
print(f"JA3 Hash: {data['tls']['ja3_hash']}")
print(f"JA3 String: {data['tls']['ja3'][:80]}...")
print(f"Cipher Suites: {len(data['tls']['ciphers'])} suites")
print(f"Extensions: {len(data['tls']['extensions'])} extensions")Other testing endpoints:
https://tls.peet.ws/api/all— Detailed TLS and HTTP/2 fingerprinthttps://tls.browserleaks.com/json— TLS fingerprint analysishttps://ja3er.com/json— JA3 hash lookup
Common Mistakes
- Mismatched UA and TLS — Sending Chrome user agent with Python TLS fingerprint
- Ignoring HTTP/2 — Matching TLS but sending HTTP/1.1 (Chrome defaults to HTTP/2)
- Static fingerprints — Using the same TLS fingerprint across millions of requests
- Old impersonation targets — Using Chrome 99 impersonation when the site checks for recent versions
- Missing GREASE — Chrome includes randomized GREASE values; static tools don’t
FAQ
Can I change my JA3 fingerprint without curl_cffi?
Standard Python libraries (requests, httpx, aiohttp) don’t allow fine-grained TLS configuration. You’d need to modify the underlying OpenSSL/BoringSSL settings, which is impractical. Use curl_cffi or tls-client — that’s exactly what they’re designed for.
Does JA3 fingerprinting work through proxies?
Yes. TLS fingerprinting happens between the client and the first TLS endpoint. When using an HTTPS proxy, the target site sees the proxy’s TLS fingerprint for the proxy connection, but your fingerprint for the target connection (via CONNECT tunnel). When using a SOCKS proxy or HTTP proxy with CONNECT, the target site sees your original TLS fingerprint.
How often do browser TLS fingerprints change?
Browser TLS fingerprints change with major version updates (roughly every 4-6 weeks for Chrome). When impersonating a browser, use a recent version target and update regularly. Sending a Chrome 99 TLS fingerprint in 2026 is suspicious.
Is JA3 fingerprinting enough to block all bots?
No. JA3 is one layer. Sophisticated scrapers using curl_cffi or real browsers produce valid JA3 hashes. Anti-bot systems combine JA3 with HTTP/2 fingerprinting, header analysis, browser fingerprinting, and behavioral signals for comprehensive detection.
Conclusion
TLS/JA3 fingerprinting is the first line of defense against web scrapers, catching requests before any page content is delivered. For Python scrapers, curl_cffi with browser impersonation is the simplest and most effective solution. Always ensure your TLS fingerprint matches your user agent, and combine TLS spoofing with residential proxies for maximum effectiveness.
Useful Resources
- JA3 GitHub Repository
- curl-impersonate GitHub
- curl_cffi Documentation
- How Websites Detect Bots
- Bypass Cloudflare with Python
- 403 Forbidden in Web Scraping: How to Fix It
- Best CAPTCHA Solving Services in 2026: Complete Comparison
- Anti-Phishing with Proxies: How Security Teams Use Mobile IPs
- Brand Protection with Proxies: Detect Counterfeit Sellers & Trademark Violations
- How Cybersecurity Teams Use Proxies for Threat Intelligence
- Using Mobile Proxies for Dark Web Monitoring and Research
- 403 Forbidden in Web Scraping: How to Fix It
- Best CAPTCHA Solving Services in 2026: Complete Comparison
- Anti-Phishing with Proxies: How Security Teams Use Mobile IPs
- Brand Protection with Proxies: Detect Counterfeit Sellers & Trademark Violations
- How Cybersecurity Teams Use Proxies for Threat Intelligence
- Using Mobile Proxies for Dark Web Monitoring and Research
Related Reading
- 403 Forbidden in Web Scraping: How to Fix It
- Best CAPTCHA Solving Services in 2026: Complete Comparison
- Anti-Phishing with Proxies: How Security Teams Use Mobile IPs
- Brand Protection with Proxies: Detect Counterfeit Sellers & Trademark Violations
- How Cybersecurity Teams Use Proxies for Threat Intelligence
- Using Mobile Proxies for Dark Web Monitoring and Research
Related: New to the concept? Start with our TLS fingerprint explained.