curl-cffi
curl-cffi is a Python HTTP client that binds to a patched build of libcurl (the maintainer's fork of curl-impersonate) through cffi. The point is not speed, it is the TLS handshake: normal Python clients like requests and httpx produce a JA3 and HTTP/2 fingerprint that anti-bot services recognise instantly, while curl-cffi can send the exact handshake a real Chrome or Safari build sends. The Python API deliberately copies requests, so get/post/Session calls look familiar, plus there is an AsyncSession, WebSocket support, HTTP/2 and HTTP/3, and a curl-cffi command line tool for debugging a single URL.
The clearest answer in Python when a site blocks you at the TLS layer, and the requests-shaped API means adopting it costs almost nothing. Treat the 0.x version number seriously, pin your dependency, and remember it does not solve JavaScript challenges.
Use it if
- A site returns 403 or a challenge page to your Python client but loads fine in a browser, and you have already checked headers and cookies
- You are writing a scraper or API client where the target inspects TLS/JA3 or HTTP/2 fingerprints, not just the User-Agent string
- You want requests-style code but need HTTP/2 or HTTP/3 support, which requests does not speak at all
- You need per-request proxy rotation on asyncio, or WebSocket support, from the same client that does your normal requests
- You are calling your own API or a public JSON API that is not blocking anyone: requests or httpx is a smaller dependency with no compiled libcurl underneath
- The block you are fighting is JavaScript based (a challenge page that runs code before issuing a cookie): matching the TLS fingerprint alone will not get you past it, and the README itself sells third party token services for that case
- You need long term API guarantees: the package is still on 0.x, is classified Development Status 4 (Beta), and minimum Python moved to 3.10 during the 0.14 cycle
- You are on a BSD system, or need Android outside of beta support, since prebuilt libcurl-impersonate wheels are not published for those
- Your legal or contractual position does not allow you to present yourself as a browser to a service that is trying to identify automated traffic
Setup reality
pip install curl_cffi pulls a prebuilt abi3 wheel with libcurl-impersonate inside, so on Linux, macOS and Windows there is nothing to compile and install takes seconds. The maintenance cost is elsewhere: browser fingerprints go stale as Chrome and Safari ship new versions, so impersonate="chrome" without a version number is the setting you usually want, and since 0.15.1 there is a curl-cffi update command that pulls newer fingerprints without a package upgrade. Some presets are free and the wider fingerprint database sits behind the maintainer's commercial impersonate.pro plan, which is worth knowing before you build a pipeline around one specific browser version.
Patterns
Fetch a URL as Chromeimpersonate-get
import curl_cffi
r = curl_cffi.get(
"https://tls.browserleaks.com/json",
impersonate="chrome",
)
print(r.status_code)
print(r.json())impersonate="chrome" without a version tracks whatever the installed build considers latest, which is what you want in long lived code.
Pin one exact browser fingerprintpin-browser-version
r = curl_cffi.get(url, impersonate="chrome124")Pinning is reproducible but ages badly: an old fingerprint eventually looks as suspicious as no fingerprint at all.
Keep cookies across requests with a Sessionsession-cookies
s = curl_cffi.Session(impersonate="chrome")
s.get("https://httpbin.org/cookies/set/foo/bar")
r = s.get("https://httpbin.org/cookies")
print(r.json())Set impersonate once on the Session instead of repeating it per call; the session also reuses the underlying curl handle.
POST JSON and read the responsepost-json
r = curl_cffi.post(
"https://httpbin.org/post",
json={"name": "demo"},
impersonate="chrome",
timeout=30,
)
r.raise_for_status()
data = r.json()The keyword arguments mirror requests, so json=, data=, headers= and timeout= behave the way you expect.
Route through an HTTP or SOCKS proxyproxies
proxies = {"https": "http://localhost:3128"}
r = curl_cffi.get(url, impersonate="chrome", proxies=proxies)
proxies = {"https": "socks://localhost:3128"}
r = curl_cffi.get(url, impersonate="chrome", proxies=proxies)On AsyncSession the proxy can be set per request, which is how rotating pools are usually wired up.
Use the async clientasync-session
from curl_cffi import AsyncSession
async def main():
async with AsyncSession(impersonate="chrome") as s:
r = await s.get("https://example.com")
print(r.status_code)AsyncSession must be entered inside a running event loop; the context manager is what closes the curl multi handle.
Fetch many URLs concurrentlyconcurrent-requests
import asyncio
from curl_cffi import AsyncSession
async def main(urls):
async with AsyncSession(impersonate="chrome") as s:
tasks = [s.get(u) for u in urls]
results = await asyncio.gather(*tasks)
return resultsBound the list with an asyncio.Semaphore for large crawls; libcurl will happily open more sockets than the target tolerates.
Request over HTTP/3http3
r = curl_cffi.get(
"https://fp.impersonate.pro/api/http3",
http_version="v3",
impersonate="chrome",
)HTTP/3 fingerprints and UDP SOCKS5 proxy support landed in 0.15.0; older installs silently fall back.
Send a fingerprint that is not a presetcustom-fingerprint
r = curl_cffi.get(
"https://tls.browserleaks.com/json",
ja3=my_ja3_string,
akamai=my_akamai_string,
)Use this when impersonating something that is not a stock browser; you supply the strings, the library does not guess them.
Open a WebSocket on the async sessionwebsocket-async
from curl_cffi import AsyncSession
async def main():
async with AsyncSession() as session:
async with session.ws_connect("wss://echo.websocket.org") as ws:
await ws.send_str("Hello")
async for message in ws:
print(message)
breakIterating the socket is how you receive; there is also a callback style WebSocket class with run_forever for synchronous code.
Debug one URL from the shellcli-debug
curl-cffi get tls.browserleaks.com/json --impersonate chrome
curl-cffi listThe CLI arrived in 0.15; install curl_cffi[cli] for coloured output, and use list to see which fingerprints your build actually has.
Refresh fingerprints without upgradingupdate-fingerprints
curl-cffi updateAvailable since 0.15.1. Safari, Chrome and Firefox updates are free; the rest of the database is part of the maintainer's paid plan.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| httpx | PyPI | You need HTTP/2 and sync plus async in a pure Python client and nothing is fingerprint blocking you |
| requests | PyPI | Ordinary synchronous HTTP calls where the default TLS fingerprint is fine |
| playwright | PyPI | The block involves JavaScript challenges, so you need a real browser engine rather than a matching handshake |
| pycurl | PyPI | You want raw libcurl bindings for speed and control, without the impersonation layer or the requests-style API |