🎯 A customizable, anti-detection cloud browser powered by self-developed Chromium designed for web crawlers and AI Agents.👉Try Now
Back to Blog

curl_cffi Web Scraping: Browser TLS Fingerprints in Python

Sophia Martinez
Sophia Martinez

Specialist in Anti-Bot Strategies

22-Jul-2026

TL;DR:

  • curl_cffi is a Python HTTP client that impersonates a real browser's TLS handshake, so a server fingerprinting the connection sees Chrome rather than a scripting client.
  • One requests.get(url, impersonate="chrome") call presents a browser JA3 fingerprint and negotiates HTTP/2, no browser process required.
  • The JA3 hash changes every run, and that is correct: GREASE inserts random values into the handshake, which real Chrome does too.
  • curl_cffi does not run JavaScript, so on a client-rendered page it returns the empty shell; the guide proves it with 0 quote blocks from curl_cffi and 10 from Scrapeless with rendering.
  • Reach for curl_cffi when a TLS fingerprint is the wall, and for Scrapeless when the page needs rendering, challenges cleared, or IP rotation.
  • Start on the Scrapeless free plan for the pages TLS impersonation alone cannot reach.

A plain Python HTTP client has a giveaway a target can read before it sends a single byte of HTML: its TLS handshake. The set of ciphers and extensions in the ClientHello does not look like a browser's, and a server that fingerprints that handshake can block the request on the fingerprint alone. curl_cffi closes that gap by borrowing a real browser's TLS signature.

This guide shows curl_cffi presenting a browser fingerprint, explains why the JA3 hash is not constant, and then draws the line curl_cffi cannot cross. Where a page needs a real browser, the Scrapeless Universal Scraping API takes over. Every number below comes from a real run.

What curl_cffi Does

curl_cffi is a requests-style HTTP client built on curl-impersonate, and its job is to make the TLS handshake look like a specific browser. A server that inspects the handshake computes a JA3 fingerprint, a summary of the ClientHello's version, ciphers, and extensions defined over the TLS 1.3 handshake. A default Python client produces a fingerprint no browser would; curl_cffi produces one a named browser does. The curl_cffi repository lists the browser versions it can impersonate.

What curl_cffi does not do is run a browser. There is no JavaScript engine, no rendering, and no challenge solving. It is a fingerprint, not a browser.

Install

curl_cffi is a single pip install.

bash Copy
pip install curl_cffi

Impersonate a Browser Fingerprint

Pass impersonate with a browser name and curl_cffi builds that browser's TLS handshake. The request below asks a fingerprinting service to report what it saw.

python Copy
from curl_cffi import requests

response = requests.get("https://tls.peet.ws/api/all", impersonate="chrome", timeout=30)
fingerprint = response.json()
print("http version:", fingerprint["http_version"])
print("ja3 hash:", fingerprint["tls"]["ja3_hash"])

The service reports HTTP/2, which a browser negotiates and a default client usually does not, and a JA3 hash.

text Copy
http version: h2
ja3 hash: 9a23d5cedcea13483954537602158034

The ja3 hash value above is one run's result; run the script again and it changes. That is not a bug. The GREASE mechanism inserts random reserved values into the ClientHello, and real Chrome does the same, so the JA3 hash differs from connection to connection. What stays browser-like is the shape of the handshake, which is what a fingerprint check actually evaluates.

Where curl_cffi Stops

A browser TLS fingerprint gets the request past a handshake check, but it does not render the page. On a site that builds its content with JavaScript, curl_cffi returns exactly what the server sent, which is an empty shell, and no amount of TLS impersonation fills it. This is where a rendering tool is required.

Set your Scrapeless key in the shell. Use the real key at run time and keep the placeholder out of your source.

bash Copy
export SCRAPELESS_API_KEY="sk_your_key_here"

The script below fetches a JavaScript-rendered quotes page two ways: curl_cffi with a perfect Chrome fingerprint, and Scrapeless with rendering on.

python Copy
import json
import os
import urllib.request

from curl_cffi import requests

JS_PAGE = "https://quotes.toscrape.com/js/"

response = requests.get(JS_PAGE, impersonate="chrome", timeout=30)
print("curl_cffi status:", response.status_code)
print("curl_cffi quote blocks:", response.text.count('class="quote"'))


def scrapeless(url: str) -> str:
    payload = json.dumps(
        {"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": True, "headless": True}}
    ).encode()
    request = urllib.request.Request(
        "https://api.scrapeless.com/api/v2/unlocker/request",
        data=payload,
        headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=90) as response:
        return json.loads(response.read())["data"]


html = scrapeless(JS_PAGE)
print("scrapeless quote blocks:", html.count('class="quote"'))

curl_cffi gets a clean 200 and zero quotes, because the quotes are drawn by JavaScript it never runs. Scrapeless renders the page and the quotes appear.

text Copy
curl_cffi status: 200
curl_cffi quote blocks: 0
scrapeless quote blocks: 10

The 200 is the trap: the request succeeds, so nothing looks wrong, but the content is not there. The two tools answer different problems. curl_cffi defeats a TLS fingerprint check; it does not render, clear a challenge, or rotate IPs. For the concept behind the fingerprint, the guide on how to bypass TLS fingerprinting covers what a server measures and why.

Before running any of this across a site, read its robots.txt and terms. The Robots Exclusion Protocol states which paths a site asks automated clients to avoid, and honoring it keeps a scraper sustainable.

Ready for the pages a fingerprint alone cannot reach? Create a free Scrapeless account and render them.

Conclusion

curl_cffi is the right tool for one job: making a Python request's TLS handshake look like a browser's, which clears a fingerprint check without a browser process. It is precise, and its limit is just as precise, because it runs no JavaScript and returns the empty shell of a client-rendered page. Use it where the wall is the handshake, and hand the page to Scrapeless when it needs rendering, a cleared challenge, or a fresh IP. Start from the scripts above and match the tool to the wall in front of you.

Start with the Scrapeless free plan for rendered pages, and check Scrapeless pricing when you size a recurring job.

FAQ

Q: What does curl_cffi actually impersonate?

curl_cffi impersonates a browser's TLS handshake and its HTTP/2 settings, so the ClientHello a server sees matches a named browser version rather than a default Python client. It does not impersonate a browser's JavaScript engine or rendering, only the network-level fingerprint.

Q: Why does the JA3 hash change every time I run the request?

Because of GREASE, which inserts random reserved values into the TLS ClientHello. Real Chrome does this too, so the JA3 hash varies between connections while the meaningful shape of the handshake stays browser-like. A changing hash is expected, not a sign the impersonation failed.

Q: Can curl_cffi scrape a JavaScript-rendered page?

No. curl_cffi has no JavaScript engine, so on a client-rendered page it returns the initial HTML shell with a 200 status and none of the rendered content. Use a rendering tool such as Scrapeless for those pages, and keep curl_cffi for endpoints whose content is already in the response.

Q: How is curl_cffi different from requests?

The requests library sends a standard Python TLS handshake that a fingerprinting server can flag, while curl_cffi sends a browser's handshake through its impersonate argument. The API is otherwise familiar, so requests.get(url, impersonate="chrome") is close to a drop-in for code that was being blocked on its fingerprint.

Q: Is a browser TLS fingerprint enough to avoid blocking?

It is enough for a TLS-fingerprint check and nothing more. Challenge pages, behavioral analysis, and IP reputation are separate defenses that curl_cffi does not touch, so when those are in play you need rendering, challenge handling, or proxy rotation on top of the fingerprint.

At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.

Most Popular Articles

Catalogue