DEV Community

Roberto Francisco junior
Roberto Francisco junior

Posted on Originally published at messora.dev

Why standard HTTP clients get blocked by anti-bot systems and how TLS fingerprinting works

If you have ever written a web scraper using Python requests, urllib3, or aiohttp, you have likely encountered this:

import requests
# Headers look identical to Chrome...
headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)..."}
response = requests.get("https://protected-site.com", headers=headers)
print(response.status_code) # 403 Forbidden
Enter fullscreen mode Exit fullscreen mode

Setting a browser User-Agent header is no longer sufficient against modern WAFs (Cloudflare, DataDome, Akamai). The block happens before your HTTP headers are even evaluated.

What is TLS Fingerprinting (JA3 / JA4)?

When a client initiates an HTTPS connection, it sends a ClientHello packet as part of the TLS handshake. This packet contains:

  1. TLS Version: (e.g., TLS 1.3, TLS 1.2)
  2. Cipher Suites: The cryptographic algorithms the client supports, in order of preference.
  3. Extensions: Supported extensions (SNI, ALPN, Supported Versions, Key Share).
  4. Elliptic Curves & Formats: Supported curves for key exchange.

Because different software stacks (OpenSSL in Python vs. BoringSSL in Chrome vs. Apple SecureTransport) build this ClientHello differently, each stack produces a distinct fingerprint.

  • Python requests (OpenSSL) has a known TLS signature.
  • Headless Chromium has a distinct signature from desktop Chrome.
  • Anti-bot systems hash these parameters into a JA4 string. If your User-Agent claims to be Chrome but your TLS handshake looks like OpenSSL, the request is dropped immediately.

How to solve TLS mismatch

1. Using TLS impersonation libraries (curl_cffi)

In Python, curl_cffi binds against a custom curl binary patched with BoringSSL to emulate exact browser handshakes:

from curl_cffi import requests

# Impersonates Chrome 120 TLS handshake and HTTP/2 settings
response = requests.get("https://protected-site.com", impersonate="chrome120")
print(response.status_code) # 200 OK
Enter fullscreen mode Exit fullscreen mode

2. Offloading to an ingestion gateway

For production workloads involving IP rotation, residential proxies, and dynamic JavaScript rendering, managing TLS profiles and browser pools adds infrastructure cost.

Services like MESSORA manage fingerprint rotation, residential routing, and markdown parsing behind an API call:

curl -X POST https://api.messora.dev/v1/extract \
  -H "Authorization: Bearer YOUR_MESSORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://protected-site.com", "only_main_content": true}'
Enter fullscreen mode Exit fullscreen mode

Understanding where the inspection happens (Network layer vs. TLS layer vs. Application layer) is key to diagnosing why requests fail.

Top comments (0)