all posts
Anti-DetectionJune 11, 2026·12 min read

Why your Python scraper gets blocked before byte one

The JA3 hash of a plain requests call is nothing like a real Chrome. curl_cffi is the fix, and this is what it actually does under the hood.

$ intelliscrape
→ resolving...
→ engine: tier-3
→ cleared
→ done

Every TLS handshake sends a fingerprint. It's a hash derived from your cipher suites, extensions, elliptic curve parameters, and signature algorithms. This fingerprint is called JA3, and it's one of the first things anti bot systems check. If your TLS fingerprint doesn't match a known browser, you're blocked before a single HTTP byte is sent. And if you're using Python's requests library, your fingerprint is immediately flagged.

The JA3 problem explained

JA3 works by hashing five fields from the TLS ClientHello message: the TLS version, cipher suites, extensions, elliptic curves, and point formats. Each browser produces a unique JA3 hash because browsers implement TLS differently. Chrome 124 has a specific set of cipher suites and extensions. Firefox 125 uses a different set. Safari 17 has yet another configuration. Python's requests library uses OpenSSL, which produces a JA3 hash that matches none of these browsers.

The result is straightforward: anti bot systems see a JA3 hash that doesn't match Chrome, Firefox, Safari, Edge, or any known browser. The connection is terminated immediately. There's no HTTP request, no response, no content to scrape. You're blocked at the transport layer.

How requests gets flagged

When you run requests.get('https://example.com'), here's what happens at the network level. Your Python process opens a TCP connection to the server. It sends a TLS ClientHello with OpenSSL's default cipher suites and extensions. The server (or a WAF sitting in front of it) hashes these fields and compares the result against known browser fingerprints. The hash doesn't match, and the connection is dropped or redirected to a challenge page.

You never see this happening because requests gives you a ConnectionError or an empty response. The blocking happens so early that most developers don't realize TLS fingerprinting is the cause. They spend hours tweaking user-agent strings, adding headers, and rotating proxies, none of which addresses the root problem.

How curl_cffi impersonates real browsers

curl_cffi is a Python binding around impersonate, a C library that can mimic the TLS fingerprint of real browsers. It doesn't just set a user-agent string. It rewrites the entire TLS ClientHello to match Chrome 124, Firefox 125, Safari 17, or any other supported browser. The cipher suites, extensions, curves, and point formats are all set to match the target browser exactly.

Under the hood, impersonate uses a modified version of OpenSSL that can produce any TLS configuration. It builds a ClientHello message with the exact byte sequence that a real browser would send. This means the JA3 hash matches perfectly. Anti-bot systems see a TLS fingerprint that's indistinguishable from a real Chrome or Firefox connection.

from intelliscrape import IntelliScrape
scraper = IntelliScrape()
result = scraper.scrape(
"https://target.com",
impersonate="chrome124"
)
# JA3 hash now matches real Chrome 124
# Connection accepted, content returned

JA4 and beyond

JA3 is being supplemented by JA4, which adds more fields to the fingerprint and uses a different hashing algorithm. JA4 is harder to spoof because it includes the order of extensions and the specific values of certain fields. IntelliScrape's curl_cffi engine supports JA4 as well, ensuring your connections match real browser fingerprints regardless of which version the anti bot system uses.

Why this matters for scraping

TLS impersonation alone handles roughly 70% of anti bot targets. Combined with proper headers, cookie handling, and redirect following, it's the single most effective technique for bypassing modern anti bot systems. If you're building a scraper and you're not doing TLS impersonation, you're leaving the easiest win on the table. The cost is minimal (curl_cffi adds a few milliseconds per request) and the benefit is enormous (you actually get content instead of empty responses).

powered by
VercelNeon DBfingerprint-oss