Scraping Central is reader-supported. When you buy through links on our site, we may earn an affiliate commission.

Guide

TLS Fingerprinting in Web Scraping - What It Is and How to Handle It

Understand TLS fingerprinting (JA3/JA4) in web scraping and learn techniques to avoid detection by matching real browser TLS signatures.

TLS fingerprinting is one of the most effective anti-bot techniques because it operates at the network level, before your scraper even receives a response. Here is how it works and what you can do about it.

What Is TLS Fingerprinting?

When your scraper connects to a website over HTTPS, it sends a TLS Client Hello message. This message contains details about supported cipher suites, extensions, elliptic curves, and protocol versions. The specific combination creates a unique fingerprint.

JA3 and its successor JA4 are hashing algorithms that generate a fingerprint from these TLS parameters. Every HTTP client has a distinct JA3/JA4 hash.

Why It Matters for Scraping

Python requests:     JA3 = 771,49195-49196-52393-49199...  (flagged as bot)
Real Chrome 136:     JA3 = 771,4865-4866-4867-49195...     (allowed)

Anti-bot systems maintain databases of known bot JA3 hashes. Python's requests library, aiohttp, httpx, and even Node.js axios all have distinctive TLS fingerprints that differ from real browsers. Sending requests with these libraries to a protected site results in immediate blocking.

Solution 1: ScraperAPI (Recommended)

ScraperAPI uses real browser TLS stacks, so fingerprinting is not an issue.

import requests

response = requests.get(
    "http://api.scraperapi.com",
    params={
        "api_key": "YOUR_SCRAPERAPI_KEY",
        "url": "https://protected-site.com"
    }
)

Solution 2: curl_cffi for TLS Impersonation

The curl_cffi Python library can impersonate real browser TLS fingerprints.

from curl_cffi import requests as cffi_requests

# Impersonate Chrome 136's TLS fingerprint
response = cffi_requests.get(
    "https://protected-site.com",
    impersonate="chrome136"
)
print(response.status_code)

Solution 3: Real Browser via Playwright

Using a real browser naturally produces a valid TLS fingerprint.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    response = page.goto("https://protected-site.com")
    print(response.status)
    browser.close()

How to Check Your TLS Fingerprint

Use these services to see what fingerprint your scraper produces:

from curl_cffi import requests as cffi_requests

# Check your JA3 hash
response = cffi_requests.get("https://tls.peet.ws/api/all", impersonate="chrome136")
data = response.json()
print(f"JA3: {data['tls']['ja3']}")
print(f"JA4: {data['tls']['ja4']}")

Key Takeaway

TLS fingerprinting catches most HTTP-library-based scrapers silently. If you are getting blocked with no obvious reason (correct headers, proper cookies, residential IP), TLS fingerprinting is likely the cause. Use curl_cffi, a real browser, or a managed API to solve it.