
When you write a scraper in Python using the requests library, set a convincing User-Agent and headers, and run it confidently—only to find the server returns 200 OK but the page content is a CAPTCHA or blank—this is usually not a coding bug.
What betrayed you was your TLS fingerprint.
Major sites like Walmart, Shopify, and Cloudflare no longer rely solely on User-Agent strings to detect crawlers. They inspect TLS handshake fingerprints—often represented as JA3 hashes—derived from TLS version, cipher suites, extension lists, and other parameters. Common HTTP clients (such as requests or urllib3) exhibit TLS fingerprints that differ noticeably from real browsers, and servers can flag that difference during the handshake and treat the connection as automated traffic.
This is where curl_cffi comes in. This article explains the technical principles, main features, practical usage, and how to combine curl_cffi with proxy IPs to achieve efficient, stealthy web data collection.
What is curl_cffi?
Definition: A Python HTTP client that can impersonate TLS fingerprints
curl_cffi provides Python bindings (via CFFI) for the curl-impersonate branch of cURL. In short, it’s an HTTP client capable of simulating browser-level TLS/JA3/HTTP2 fingerprints.
Against TLS fingerprint–based anti-bot systems, curl_cffi is a practical solution. It alters the underlying cURL TLS behavior so that the handshake fingerprint matches a real browser by performing changes such as:
- TLS library substitution: use browser-like TLS implementations (for example BoringSSL) instead of cURL’s default
- Configuration adjustments: modify TLS extensions and SSL options to mimic browser behavior
- HTTP/2 tuning: match browser HTTP/2 handshake patterns and SETTINGS parameters
- Custom cURL options: apply specific ciphers, curves, and header handling for finer-grained impersonation
Why impersonate a TLS fingerprint?
When you make an HTTPS request, a TLS handshake occurs. During that handshake the client and server negotiate cipher suites, TLS version, extensions, and more. Those parameters create a distinctive TLS fingerprint, commonly expressed as a JA3 hash.
Standard HTTP clients differ from browsers in the values they send during the handshake:
- Different cipher suite lists
- Different extension order
- Different supported protocol versions
These differences allow servers to identify “non-browser” traffic during the handshake and respond with CAPTCHAs or deny access. Like using a proxy to hide your IP, curl_cffi lets your client “speak” with a browser’s TLS voice so the server treats the request like a real browser session.
Core features of curl_cffi
Overview of capabilities
curl_cffi provides the following core capabilities:
| Feature | Description |
| JA3 / TLS fingerprint impersonation | Supports built-in and custom fingerprints to mimic Chrome, Edge, Firefox, Safari |
| HTTP/2 impersonation | Matches browser HTTP/2 handshake behavior and SETTINGS frame parameters |
| High performance | Faster than requests and httpx, comparable to aiohttp |
| Requests-style API | Familiar API design similar to requests for quick onboarding |
| Async support | Supports asyncio for asynchronous requests |
| Proxy rotation | Built-in support for rotating proxies per request |
| Native HTTP/2 | Native support for HTTP/2 protocol |
| WebSocket support | Supports WebSocket connections for real-time streams |
Supported browser fingerprints
curl_cffi includes fingerprint presets for major browsers and versions:
- Chrome (multiple versions)
- Edge (multiple versions)
- Firefox (multiple versions)
- Safari (multiple versions)
You can select a browser fingerprint with a simple parameter instead of manually configuring low-level TLS options.
Comparison with requests / httpx / aiohttp
| Dimension | requests | httpx | aiohttp | curl_cffi |
| TLS fingerprint impersonation | ✖ Not supported | ✖ Not supported | ✖ Not supported | ✔ Native support |
| JA3 impersonation | ✖ Not supported | ✖ Not supported | ✖ Not supported | ✔ Native support |
| Synchronous API | ✔ Excellent | ✔ Supported | ✖ Not supported | ✔ Excellent |
| Asynchronous API | ✖ Not supported | ✔ Supported | ✔ Excellent | ✔ Supported |
| HTTP/2 | ✖ Not supported | ✔ Supported | ✔ Supported | ✔ Supported |
| Proxy rotation | ⚠ Manual | ⚠ Manual | ⚠ Manual | ✔ Built-in |
| Execution speed | Medium | Medium | Fast | Fast (comparable to aiohttp) |
The main advantage of curl_cffi is that it retains the ease of requests while delivering aiohttp-like performance and providing a native solution to TLS fingerprint detection—an area where requests/httpx lack capabilities.
curl_cffi in practice: installation and bypassing anti-bot
Below is a practical walkthrough for scraping a site protected by a web application firewall (WAF) using curl_cffi.
Environment setup
Ensure Python 3+ is installed. Create a project directory, set up a virtual environment, and install curl_cffi:
mkdir curl-cffi-scraper
cd curl-cffi-scraper
python -m venv env
source env/bin/activate # Windows: env\Scripts\activate
pip install curl_cffi
Basic usage: impersonating Chrome
Here is a minimal example using curl_cffi:
from curl_cffi import requests
# Use Chrome 120 TLS fingerprint to make a request
response = requests.get(
'https://www.walmart.com/s?keyword=keyboard',
impersonate='chrome120'
)
print(response.status_code) # 200
print(response.text[:500]) # expected page content
The key is impersonate='chrome120', which instructs curl_cffi to use the full TLS/HTTP2 fingerprint of Chrome 120. A standard requests.get() call might return 200 but present a CAPTCHA page because the server detected the client’s TLS fingerprint and flagged it as non-browser traffic.
List of supported fingerprints
To list supported fingerprints programmatically:
from curl_cffi.requests import BrowserType
# Print all supported browser types
print(BrowserType)
Common presets include chrome110, chrome120, edge101, firefox102, and safari15_5.
Asynchronous usage
For concurrent requests, curl_cffi supports async operations:
import asyncio
from curl_cffi import requests
async def fetch_page(url):
response = await requests.aget(
url,
impersonate='chrome120'
)
return response.text
async def main():
urls = ['https://www.walmart.com/s?keyword=keyboard'] * 10
tasks = [fetch_page(url) for url in urls]
results = await asyncio.gather(*tasks)
print(f"Successfully fetched {len(results)} pages")
asyncio.run(main())
In async mode, curl_cffi can issue many simultaneous requests with performance comparable to aiohttp.
curl_cffi + proxy IPs: building production-grade crawlers
In real-world data collection, TLS fingerprint impersonation alone is insufficient. Target sites also enforce rate limits and bans based on IP behavior. Combining curl_cffi’s fingerprint impersonation with a high-quality proxy pool is the complete strategy for bypassing anti-bot defenses.
Proxy configuration in curl_cffi
curl_cffi supports proxy usage per request, with configuration similar to requests:
from curl_cffi import requests
proxies = {
'http': 'http://username:password@proxy_host:port',
'https': 'http://username:password@proxy_host:port',
}
response = requests.get(
'https://www.walmart.com/s?keyword=keyboard',
impersonate='chrome120',
proxies=proxies
)
Dynamic proxy rotation
For large-scale collections, rotate proxies from a proxy pool automatically:
import random
from curl_cffi import requests
proxy_list = [
'http://user1:pass1@proxy1:8080',
'http://user2:pass2@proxy2:8080',
'http://user3:pass3@proxy3:8080',
]
def get_random_proxy():
return {'http': random.choice(proxy_list), 'https': random.choice(proxy_list)}
for i in range(100):
proxies = get_random_proxy()
response = requests.get(
'https://www.walmart.com/s?keyword=keyboard',
impersonate='chrome120',
proxies=proxies
)
# process the response...
Proxy rotation prevents rate limits and long-term IP bans by distributing requests across many exit addresses.
Choosing proxies: residential vs data center
Proxy type matters. Data center IPs come from cloud providers or hosting facilities and often reveal their ASN as data center networks. Even with perfect TLS impersonation, traffic from a known data center IP can trigger stricter defenses.
Residential proxies come from ISPs and appear as ordinary home user addresses. When a browser-level TLS fingerprint is combined with a residential IP, the request closely resembles legitimate user traffic and is much less likely to trigger anti-bot systems.
Residential IPs carry reputation, geography, ASN, and usage history. They blend into background internet traffic more naturally than data center addresses, making them preferable for large-scale, stealthy scraping tasks. For long-running sessions where continuity matters, sticky residential IPs can maintain consistent identity across requests.
Advanced curl_cffi usage
Custom fingerprints
Besides built-in fingerprints, curl_cffi allows further customization of TLS parameters for specialized use cases:
from curl_cffi import requests
response = requests.get(
'https://example.com',
impersonate='chrome120',
# further customize JA3/TLS parameters if needed
)
Handling login-protected pages
curl_cffi supports session management and cookies for authenticated workflows:
from curl_cffi import requests
session = requests.Session(impersonate='chrome120')
# Login
login_response = session.post(
'https://example.com/login',
data={'username': 'user', 'password': 'pass'}
)
# Access authenticated page with the same session
profile_response = session.get('https://example.com/profile')
WebSocket support
curl_cffi also supports WebSocket connections for scenarios that require real-time data streams; consult the library documentation for specific examples and options.

curl_cffi — a crucial piece for anti-bot evasion in Python scraping
curl_cffi fills a critical gap in the Python scraping ecosystem by addressing TLS fingerprint impersonation. Previously, developers faced a trade-off: use requests or httpx for speed and convenience but risk detection, or use browser automation tools like Selenium or Playwright to avoid fingerprinting at the cost of speed and resource usage.
curl_cffi offers a third path: lightweight, high-performance, and highly convincing impersonation. It makes the TLS handshake appear identical to a real browser while preserving a requests-style API and delivering aiohttp-like throughput.
However, TLS fingerprinting is only half the battle: identity of origin (IP) matters too. Coupling browser-level TLS fingerprints with high-quality residential proxies produces the most realistic combination—requests look like they come from real browsers on real home networks.

Provisioning professional proxy resources for your curl_cffi crawlers
curl_cffi addresses TLS fingerprinting, while proxy quality determines the long-term stability and success of collection tasks. A broad, reputable proxy network that supports flexible rotation is essential for sustained operation of curl_cffi-based crawlers.
Choose proxy providers that offer global coverage, residential and data center options, sticky session support, and protocols like HTTP, HTTPS, and SOCKS5. Integrate those proxy endpoints into curl_cffi via the proxies parameter to build robust, scalable scraping pipelines.