curl_cffi and Cloudflare Turnstile
curl_cffi clears one specific obstacle very well: it makes your TLS and HTTP/2 fingerprints look like a real browser's, which plain requests and httpx cannot do. What it cannot do is run a challenge, and this is not a limitation to work around — the project's own FAQ states it has "no browser or JavaScript runtime under the hood". Knowing exactly where that line sits saves a lot of time.
What it is, precisely
curl_cffi is a Python binding to a fork of curl-impersonate, via cffi. Its README puts the value proposition in one sentence: "Unlike other pure python http clients like httpx or requests, curl_cffi can impersonate browsers' TLS/JA3 and HTTP/2 fingerprints. If you are blocked by some website for no obvious reason, you can give curl_cffi a try."
That is a real capability and a real gap in the ecosystem. A TLS handshake has a fingerprint — cipher order, extensions, curves — and so does an HTTP/2 connection preface. Python's usual clients produce fingerprints that match no shipping browser, which is trivially detectable and says nothing about intent. curl_cffi fixes exactly that.
The line, in the project's own words
From the impersonation FAQ, on whether JavaScript fingerprints can be changed:
"No, you can not. As the name suggests, JavaScript fingerprints are generated using JavaScript APIs provided by real browsers. curl_cffi is a python binding to a C library, with no browser or JavaScript runtime under the hood."
And from the main FAQ, answering the Cloudflare question directly — worth quoting at length because it is more honest than most vendor documentation on this subject:
"Short answer is: it depends. TLS and http2 fingerprints are just one of the many factors Cloudflare considers. Other factors include but are not limited to: IP quality, request rate, JS fingerprints, etc. There are different protection levels for website owners to choose. For the most basic ones, TLS fingerprints alone maybe enough, but for higher levels, you may need to find a better proxy IP provider and use browser automation tools like playwright."
That is the whole map in four sentences. TLS is one factor. IP quality is another. JS fingerprints are a third. At low protection levels the first alone can be enough; above that it is not.
Why that rules out solving Turnstile in-process
A Turnstile challenge is a JavaScript program. api.js loads from challenges.cloudflare.com, runs a series of browser-environment probes, exchanges messages with Cloudflare inside an iframe, and eventually writes a token into a hidden cf-turnstile-response field. There is no HTTP request you can replay to skip that, because the token is the output of running the program.
Cloudflare states the same thing from their side. Their supported-browsers page lists as unsupported: "Command-line tools such as wget, curl, or others that lack JavaScript execution capabilities required for Cloudflare Challenges." curl_cffi is a binding to curl. It is named in the category.
So the sequence when you point it at a page with Turnstile is: the TLS handshake looks right, the request goes through, and you receive the challenge HTML — which is markup plus a script tag, and no data. The fingerprinting worked. There was nothing behind it to fetch.
Using it well for the part it does solve
Impersonation is a keyword argument. Prefer a bare browser name to pin the latest available version rather than freezing on an old one:
import curl_cffi # `chrome` tracks the latest available target; pin a version only when you # need reproducibility more than you need currency. r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome") print(r.json()) # http and socks proxies both work proxies = {"https": "http://localhost:3128"} r = curl_cffi.get( "https://tls.browserleaks.com/json", impersonate="chrome", proxies=proxies, )
The project publishes 37 preset targets, currently spanning chrome99 through chrome146, Firefox to firefox147, Safari to safari2601, plus Android, iOS and Tor variants. Its own guidance is to use the latest Chrome or Safari, and a version suffixed a — chrome133a — means an alternative fingerprint observed through A/B testing rather than an official release.
One behaviour that surprises people: setting impersonate also sets matching default headers. If you pass your own headers they override the defaults; default_headers=False turns them off entirely. Sending a Chrome TLS fingerprint alongside a header set no Chrome ever produced is a worse position than not impersonating at all.
# Sessions carry cookies between requests, which is what you want when a # clearance cookie is in play. with curl_cffi.Session() as s: s.get("https://httpbin.org/cookies/set/foo/bar") r = s.get("https://httpbin.org/cookies") print(r.json()) # Async requires a session. from curl_cffi import AsyncSession async with AsyncSession() as s: r = await s.get("https://example.com")
The pattern that actually works
Because curl_cffi cannot produce a token and does not need to produce one itself, the working shape is to obtain the token elsewhere and keep curl_cffi for the requests around it. That keeps the thing it is good at — a browser-shaped connection, at HTTP-client speed and memory — and delegates only the part that genuinely requires a browser.
import os import curl_cffi SITEKEY = "0x4AAAAAAA_target" PAGE = "https://app.example.com/login" # 1. Get a token. The solve does not happen from this process, so the # fingerprint of this client is irrelevant to it. solve = curl_cffi.post( "https://api.solvegate.io/v1/solve", headers={"Authorization": f"Bearer {os.environ['SOLVEGATE_KEY']}"}, json={"gate": "turnstile", "sitekey": SITEKEY, "url": PAGE}, impersonate="chrome", ).json() # 2. Submit it the way the widget would have, in the same session, so the # TLS fingerprint and any cookies stay consistent across the exchange. with curl_cffi.Session(impersonate="chrome") as s: r = s.post(PAGE, data={ "username": "...", "password": "...", "cf-turnstile-response": solve["token"], }) print(r.status_code)
Two details that decide whether this works. The field name is cf-turnstile-response unless the page renamed it with data-response-field-name — our checker reports the real name along with the sitekey. And the token is single-use with a lifetime measured in minutes, so request it at the moment you submit rather than at the start of a job; token lifetime has the exact rules.
A token typically comes back in under 1.5 seconds. Prepaid credits start at $0.40 per 1,000 and fall to $0.075 at volume, failed solves are never billed, and the first 1,000 are free. Solve only against properties you own or are authorised to test.
When curl_cffi is the wrong tool entirely
- The data is assembled client-side. No fingerprint helps if the page ships a shell and fetches its content from an API. Find that API — it is usually simpler than the page.
- A full-page interstitial, not a widget.
__cf_chl_rt_tkin the URL and Cloudflare's own markup means a WAF challenge, which has no sitekey. See Turnstile vs Cloudflare challenge. - You need a screenshot, or the DOM after scripts run. That is a browser's job by definition.
- It works locally and fails on your server. That is usually not fingerprinting at all — see Turnstile fails on a datacenter IP, where the project's own FAQ line about IP quality does most of the explaining.
Common questions
No, and the project says so itself. Its FAQ states it is a Python binding to a C library "with no browser or JavaScript runtime under the hood", and a Turnstile token is the output of running JavaScript. What it can do is make your TLS and HTTP/2 fingerprints match a real browser's, which is a different obstacle.
Sometimes, and the project's own answer is the honest one: it depends on the protection level. Their FAQ says TLS and HTTP/2 fingerprints are one of several factors alongside IP quality, request rate and JS fingerprints — enough on their own at the most basic levels, and not enough above that.
A bare browser name — chrome, firefox, safari, chrome_android, safari_ios — so you track the latest available fingerprint instead of freezing on an old one. Pin a version only when reproducibility matters more than currency. A target suffixed with a is an alternative fingerprint observed through A/B testing rather than an official browser release.
Usually not, and adding the wrong ones is actively harmful. Setting impersonate installs matching default headers; your own headers override them, and default_headers=False disables them entirely. A Chrome TLS fingerprint paired with headers no Chrome sends is more distinctive than not impersonating at all.
Post it as the form field the widget would have filled — cf-turnstile-response, unless the page renamed it with data-response-field-name. Send it in the same session you used for the rest of the exchange so the fingerprint and cookies stay consistent, and request the token at the moment you submit, since it is single-use and expires in minutes.
Related
Sources
More in Guides
Automating a gate you own or are authorised to test?
// SolveGate clears Cloudflare Turnstile and WAF challenges through one REST call · first 1,000 solves free