← All articles

curl_cffi vs a real browser, and how to tell which one a site needs

4 min read scrapingtlsautomation

Your requests script gets blocked instantly, you swap in curl_cffi with impersonate="chrome", and half your targets start answering. The other half still return a challenge page. Both results are correct. Impersonation fixed one layer, and the sites that still block you are checking something above it.

What impersonation actually reproduces

curl_cffi is a Python HTTP client built on a fork of curl-impersonate. Its README describes it as able to mimic browsers' TLS/JA3 and HTTP/2 fingerprints, selected with an impersonate argument such as "chrome" or "safari", and it accepts custom ja3 and akamai strings for targets it does not ship. tls-client does the same job in Go, with client profiles for common browsers, control over header order, and FFI bindings for Python, Node.js and C#.

What they reproduce is the part of a request that a library normally gets wrong by construction:

If a site's bot check runs at the network edge and stops there, that is the whole check.

What it cannot reproduce

Everything that requires executing a page.

JavaScript challenges. Many anti-bot products answer a suspicious first request with a page that runs a script, collects signals, and posts the result back before issuing a clearance cookie. An HTTP library receives that page as text. curl_cffi's own README points users to a separate project for JavaScript challenges, which is an honest description of the boundary.

Canvas, WebGL, audio and fonts. These signals exist only inside a rendering engine. A detector that asks for a canvas hash gets nothing from a client that has no canvas, and a missing answer is a strong answer.

Behaviour. Pointer movement, scroll, focus and timing between events are collected by page scripts. A library sends no events at all.

The request pattern itself. A browser loading a page also fetches its scripts, stylesheets, fonts and images, in an order the page dictates. A client that requests the HTML, then jumps straight to an internal API, makes a traffic shape no browser makes, even if every individual request is a perfect impersonation.

Session state beyond cookies. Sites that keep state in localStorage, IndexedDB or a passkey cannot be served by a cookie jar.

The hybrid, and where it breaks

A common pattern is to solve the challenge once in a real browser, copy the clearance cookies into curl_cffi, and do the volume work there. It can work, with conditions. Clearance cookies are often scoped to the client that earned them, so the impersonation target has to match the browser that solved the challenge, the exit IP has to stay the same, and the User-Agent you send has to name the same browser version the TLS profile imitates. Impersonating one Chrome release in the handshake while the header claims another is a contradiction you created yourself.

Where the library is the better choice

A full browser process costs hundreds of megabytes and seconds per page. An impersonating HTTP client costs a fraction of that and runs thousands of requests per minute from one machine.

Pick the library when:

A browser-based tool, AntiBrow included, is the wrong purchase for that workload. You would pay in memory and speed for layers the target never looks at.

Where you need a real browser

Pick a browser when the site serves a JavaScript challenge, when the data only appears after client-side rendering, when the work is account-based and the session has to survive across runs, or when the site reads canvas, WebGL or behaviour. In that case impersonation is not a cheaper version of the same thing. It is missing the layer being checked.

If that is where you are, AntiBrow gives each profile its own fingerprint, applied in the browser engine below the JS layer and persisted with the profile, and returns a real Playwright Page to drive it. Local profiles are unlimited on every plan, so testing it costs nothing. The SDK docs have the launch() call.

A ten-minute test for your target

  1. Open the target page in normal Chrome with JavaScript disabled in site settings. If the content you need is there, a library can probably get it.
  2. Fetch https://tls.browserleaks.com/json with your library and with Chrome, and compare the JA3, JA4 and Akamai fields. Fix any difference before blaming anything else.
  3. Request the target with the library. An instant 403 with no body points at the network layer or the IP. A 200 containing a challenge script points at the JavaScript layer, which no impersonation setting will fix.
  4. Repeat from the same exit IP you will use in production, since IP reputation can produce either result on its own. For that side of it, see residential vs datacenter proxies.

Try it on the free tier.

Unlimited local profiles, no credit card. Check it against the detectors yourself.