mrkeyoor.com_
Fri 07 Aug 22:55 UTC
PyPIUtilsupdated 07 Aug 2026

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.

Verdict

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.

API stability3/5Still pre-1.0 and classified Development Status 4 (Beta) on PyPI; the minimum Python moved to 3.10 in the 0.14 cycle and 95 releases include a steady stream of betas, so minor bumps can carry breaking changes
Docs4/5curl-cffi.readthedocs.io covers impersonation, WebSockets and the CLI, and the README is unusually candid about what fingerprinting does and does not fix; the full preset list now lives in the separate curl-impersonate docs, so you follow one hop to answer 'which browsers are supported'
Maintenance5/50.16.0 shipped 2026-08-01 and the repo was pushed 2026-08-06, with 38 open issues against 6.2k stars; the project is funded by sponsors and a commercial arm, which is why fingerprints get refreshed at all
Ecosystem4/58.7M weekly downloads, third party Scrapy integrations, and adapters that let requests or httpx borrow the transport; still a much smaller surrounding ecosystem than requests or httpx themselves

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
Skip it if

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 results

Bound 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)
                break

Iterating 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 list

The 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 update

Available 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

PackageRegistryPick it when
httpxPyPIYou need HTTP/2 and sync plus async in a pure Python client and nothing is fingerprint blocking you
requestsPyPIOrdinary synchronous HTTP calls where the default TLS fingerprint is fine
playwrightPyPIThe block involves JavaScript challenges, so you need a real browser engine rather than a matching handshake
pycurlPyPIYou want raw libcurl bindings for speed and control, without the impersonation layer or the requests-style API