[{"data":1,"prerenderedAt":106},["ShallowReactive",2],{"$fBttfDrgvBjCam2Y7q7AwuMT2NREqjMpYwpTtr11miPg":3},{"title":4,"date":5,"dateModified":6,"datePublished":7,"dateModifiedISO":7,"image":8,"content":9,"faq":10,"metaTitle":42,"metaDescription":43,"author":44,"authorBio":6,"authorLinkedin":6,"authorTitle":6,"authorPhoto":45,"lastReviewed":6,"noindex":46,"researchBasis":6,"category":47,"readingTime":48,"related":49,"prev":66,"next":6,"toc":67,"takeaways":100},"curl_cffi and TLS Fingerprint Impersonation for Web Scraping (2026)","13 Oct 2026",null,"2026-10-13","/img/news/curl-cffi-tls-fingerprint-web-scraping-2026.png","\u003Cp>\u003Cstrong>Your scraper is often refused before it sends a single request header.\u003C/strong> The TLS handshake that opens the connection carries enough detail to identify the library that made it, and a plain Python \u003Ccode>requests\u003C/code> call announces itself there — well before any JavaScript, any user-agent string or any page content comes into play.\u003C/p>\n\u003Cp>\u003Ca href=\"https://pypi.org/project/curl-cffi/\">curl_cffi\u003C/a> fixes that one layer by replaying a real browser&#39;s handshake at the socket level. It is not a stealth browser and it does not pretend to be. It is the cheapest large win available on some anti-bot vendors, and almost useless on others, and the difference is worth understanding before you install anything.\u003C/p>\n\u003Ch2 id=\"choose-before-you-read\">Choose Before You Read\u003C/h2>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Your situation\u003C/th>\n\u003Cth>Do this\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode>requests\u003C/code> gets 403s and the data is in the HTML\u003C/td>\n\u003Ctd>Use curl_cffi. This is exactly its case.\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Blocked by Akamai Bot Manager\u003C/td>\n\u003Ctd>Use curl_cffi — 87% measured, \u003Ca href=\"/blogs/playwright-akamai-bot-manager-2026\">beats a stealth browser\u003C/a>\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Blocked by standard Cloudflare\u003C/td>\n\u003Ctd>Worth trying: 79%. A stealth browser is better if it fails.\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Blocked by PerimeterX\u003C/td>\n\u003Ctd>41%. \u003Ca href=\"/blogs/bypass-perimeterx-human-web-scraping-2026\">Nothing we tested clears it reliably\u003C/a>\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>The page is empty without JavaScript\u003C/td>\n\u003Ctd>Wrong tool — \u003Ca href=\"/blogs/what-is-patchright-guide-2026\">use a patched browser\u003C/a>\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>You are on Go, Node or .NET\u003C/td>\n\u003Ctd>\u003Ca href=\"#outside-python\">There are equivalents\u003C/a>\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>You want the rows, not the stack\u003C/td>\n\u003Ctd>\u003Ca href=\"#the-maintenance-you-are-signing-up-for\">Last section\u003C/a>\u003C/td>\n\u003C/tr>\n\u003C/tbody>\u003C/table>\n\u003Caside class=\"article__usecase-card\">\u003Cdiv class=\"article__usecase-label\">Related use case\u003C/div>\u003Ch3 class=\"article__usecase-title\">Any-site data scraper\u003C/h3>\u003Cp class=\"article__usecase-blurb\">No-code extraction from any website. Managed infrastructure, no anti-bot headaches.\u003C/p>\u003Ca class=\"article__usecase-link\" href=\"/use-cases/data-scraper\">See how it works →\u003C/a>\u003C/aside>\u003Ch2 id=\"what-a-tls-fingerprint-actually-is\">What a TLS Fingerprint Actually Is\u003C/h2>\n\u003Cp>When a client opens an HTTPS connection it sends a \u003Ccode>ClientHello\u003C/code>: the TLS versions it supports, its cipher suites in preference order, its extensions, its elliptic curves, its ALPN protocols. \u003Ca href=\"https://datatracker.ietf.org/doc/html/rfc8446\">RFC 8446\u003C/a> specifies what may appear; it does not specify the exact set or the order, so every TLS implementation picks differently.\u003C/p>\n\u003Cp>Hash that set and you have a stable identifier for the implementation. That is \u003Ca href=\"https://github.com/salesforce/ja3\">JA3\u003C/a>, and its successor \u003Ca href=\"https://github.com/FoxIO-LLC/ja4\">JA4\u003C/a>, which is more robust to the cipher-shuffling that defeats JA3.\u003C/p>\n\u003Cp>The consequence is the whole point of this post:\u003C/p>\n\u003Cul>\n\u003Cli>Chrome has a handshake. Firefox and Safari have different ones.\u003C/li>\n\u003Cli>Python&#39;s \u003Ccode>requests\u003C/code> — which sits on OpenSSL via \u003Ccode>urllib3\u003C/code> — has its own, and it looks nothing like a browser&#39;s.\u003C/li>\n\u003Cli>That hash is globally known. Anti-bot vendors do not need to detect you; they recognise you.\u003C/li>\n\u003Cli>\u003Cstrong>It is read before your request line.\u003C/strong> Your \u003Ccode>User-Agent: Mozilla/5.0...\u003C/code> header has not been transmitted yet when the decision is available.\u003C/li>\n\u003C/ul>\n\u003Cp>This is why &quot;I set a realistic user-agent and it still gets blocked&quot; is the most common confusion in scraping. You dressed the request. The handshake had already introduced you.\u003C/p>\n\u003Cp>Before you change anything, see what you currently look like. \u003Ca href=\"https://tls.browserleaks.com/json\">tls.browserleaks.com/json\u003C/a> returns your JA3 and JA4 hashes as JSON, so a one-line script tells you whether the problem is really the handshake:\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"code-block__copy\" data-copy-code aria-label=\"Copy code\">Copy\u003C/button>\u003Cpre>\u003Ccode class=\"language-python\">import requests\nprint(requests.get(&quot;https://tls.browserleaks.com/json&quot;).json()[&quot;ja3_hash&quot;])\n\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>Run the same call through a browser and compare. If the hashes differ, every header you set afterwards is decoration.\u003C/p>\n\u003Ch2 id=\"what-curlcffi-does-about-it\">What curl_cffi Does About It\u003C/h2>\n\u003Cp>\u003Ca href=\"https://github.com/lwthiker/curl-impersonate\">curl-impersonate\u003C/a> is a build of curl patched to produce byte-identical handshakes to specific real browser versions. Three things change, and the third is the one most write-ups miss:\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>The TLS library is swapped.\u003C/strong> OpenSSL out, \u003Cstrong>BoringSSL\u003C/strong> in for Chrome targets and \u003Cstrong>NSS\u003C/strong> for Firefox targets — the libraries those browsers actually ship. A fingerprint is a property of the implementation, so you cannot fake it convincingly from a different implementation.\u003C/li>\n\u003Cli>\u003Cstrong>The \u003Ccode>ClientHello\u003C/code> is reordered.\u003C/strong> Same cipher suites, same extensions, in the same sequence the real browser emits.\u003C/li>\n\u003Cli>\u003Cstrong>The HTTP/2 fingerprint is matched too.\u003C/strong> Header table size, initial window size and stream priority are all set to the browser&#39;s values. Vendors fingerprint the HTTP/2 \u003Ccode>SETTINGS\u003C/code> frame as well as the TLS handshake, which is why a correct JA3 with default HTTP/2 settings still gets caught on the stricter vendors.\u003C/li>\n\u003C/ol>\n\u003Cp>\u003Ca href=\"https://github.com/lexiforest/curl_cffi\">curl_cffi\u003C/a> is Python bindings over that build, with an API deliberately shaped like \u003Ccode>requests\u003C/code>.\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"code-block__copy\" data-copy-code aria-label=\"Copy code\">Copy\u003C/button>\u003Cpre>\u003Ccode class=\"language-bash\">pip install curl_cffi\n\u003C/code>\u003C/pre>\u003C/div>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"code-block__copy\" data-copy-code aria-label=\"Copy code\">Copy\u003C/button>\u003Cpre>\u003Ccode class=\"language-python\">from curl_cffi import requests\n\nr = requests.get(&quot;https://example.com&quot;, impersonate=&quot;chrome&quot;)\nprint(r.status_code, len(r.text))\n\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>That is the entire change for a simple call. The \u003Ccode>impersonate\u003C/code> argument selects which browser build to mimic, and it accepts version-pinned targets as well as the floating alias — pin the version if you want reproducible behaviour, since the floating alias moves as the library tracks new browser releases.\u003C/p>\n\u003Cp>A session, which is what you actually want for anything with cookies:\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"code-block__copy\" data-copy-code aria-label=\"Copy code\">Copy\u003C/button>\u003Cpre>\u003Ccode class=\"language-python\">from curl_cffi import requests\n\nproxies = {&quot;https&quot;: &quot;http://user:pass@residential-endpoint:port&quot;}\n\nwith requests.Session(impersonate=&quot;chrome&quot;, proxies=proxies) as s:\n    s.get(&quot;https://example-retailer.de/&quot;)          # pick up cookies normally\n    r = s.get(&quot;https://example-retailer.de/p/12345&quot;)\n    print(r.status_code)\n\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>Your existing \u003Ccode>requests\u003C/code> code — headers, params, \u003Ccode>json=\u003C/code>, timeouts, session cookies — ports across with the import and the one argument.\u003C/p>\n\u003Ch3 id=\"when-a-preset-is-not-enough\">When a preset is not enough\u003C/h3>\n\u003Cp>The preset names cover the common cases. If a target has flagged the exact Chrome build everyone impersonates, you can specify the fingerprint directly instead:\u003C/p>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"code-block__copy\" data-copy-code aria-label=\"Copy code\">Copy\u003C/button>\u003Cpre>\u003Ccode class=\"language-python\">from curl_cffi import requests\n\nr = requests.get(\n    &quot;https://example.com&quot;,\n    ja3=&quot;771,4865-4866-4867-49195-49199,0-23-65281-10-11,29-23-24,0&quot;,\n    akamai=&quot;1:65536,2:0,4:6291456,6:262144|15663105|0|m,a,s,p&quot;,\n)\n\u003C/code>\u003C/pre>\u003C/div>\n\u003Cp>\u003Ccode>ja3=\u003C/code> sets the TLS fingerprint, \u003Ccode>akamai=\u003C/code> the HTTP/2 one, and \u003Ccode>extra_fp=\u003C/code> covers the remaining signature knobs such as signature algorithms and certificate compression. This is a last resort, not a starting point — a hand-built fingerprint that does not correspond to any real browser is worse than a shared one, because &quot;nothing on earth produces this&quot; is an easier call for a vendor than &quot;one of 400 million Chrome users&quot;.\u003C/p>\n\u003Caside class=\"article__inline-cta\">\u003Cp class=\"article__inline-cta-text\">Try ScrapeWise on your own URL. \u003Cstrong>Your first 5 requests are free.\u003C/strong>\u003C/p>\u003Ca class=\"article__inline-cta-btn\" href=\"https://portal.scrapewise.ai/register\" target=\"_blank\" rel=\"noopener\">Start Free →\u003C/a>\u003C/aside>\u003Ch2 id=\"what-it-is-worth-measured\">What It Is Worth, Measured\u003C/h2>\n\u003Cp>April 2026, 500 requests per approach over six hours against production targets, residential pool of 2M+ IPs.\u003C/p>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Anti-bot system\u003C/th>\n\u003Cth>\u003Ccode>requests\u003C/code> + datacentre\u003C/th>\n\u003Cth>curl-cffi + datacentre\u003C/th>\n\u003Cth>curl-cffi + residential\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>Akamai Bot Manager v4\u003C/td>\n\u003Ctd>22%\u003C/td>\n\u003Ctd>71%\u003C/td>\n\u003Ctd>\u003Cstrong>87%\u003C/strong>\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Cloudflare (standard)\u003C/td>\n\u003Ctd>19%\u003C/td>\n\u003Ctd>61%\u003C/td>\n\u003Ctd>\u003Cstrong>79%\u003C/strong>\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>Cloudflare Enterprise\u003C/td>\n\u003Ctd>4%\u003C/td>\n\u003Ctd>38%\u003C/td>\n\u003Ctd>\u003Cstrong>52%\u003C/strong>\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>PerimeterX / HUMAN\u003C/td>\n\u003Ctd>11%\u003C/td>\n\u003Ctd>29%\u003C/td>\n\u003Ctd>\u003Cstrong>41%\u003C/strong>\u003C/td>\n\u003C/tr>\n\u003C/tbody>\u003C/table>\n\u003Cp>Read it in two directions. \u003Cstrong>Across a row:\u003C/strong> the handshake alone is worth roughly 40–50 points on every vendor — the largest single-change gain in this table, and it costs you one keyword argument. \u003Cstrong>Down the column:\u003C/strong> the ceiling is set by the vendor, not by you. 87% on Akamai and 41% on PerimeterX come from identical code.\u003C/p>\n\u003Cp>The Akamai row is the one that surprises people, because 87% is higher than a patched stealth browser manages on the same targets. Akamai ranks TLS fingerprinting as its \u003Cem>primary\u003C/em> signal, so fixing layer one fixes most of the problem. Cloudflare weights JavaScript challenges more heavily, which is why a browser catches up there. Full cross-vendor comparison: \u003Ca href=\"/blogs/bypass-cloudflare-akamai-perimeterx-web-scraping-2026\">bypassing Cloudflare, Akamai and PerimeterX\u003C/a>.\u003C/p>\n\u003Cp>Note also that datacentre proxies cap every single row. A perfect handshake from a pre-flagged ASN is still refused at the edge.\u003C/p>\n\u003Ch2 id=\"the-four-things-it-cannot-do\">The Four Things It Cannot Do\u003C/h2>\n\u003Cp>\u003Cstrong>It runs no JavaScript.\u003C/strong> This is the hard boundary, not a limitation to work around. If the price is written into the DOM by a client-side script, curl_cffi returns the markup without it. Check before you commit: fetch the page and search the raw HTML for a known value. Plenty of large e-commerce sites embed everything in JSON-LD or a hydration payload and need no browser at all — and plenty do the opposite.\u003C/p>\n\u003Cp>\u003Cstrong>It cannot solve a challenge widget.\u003C/strong> Turnstile, a DataDome interstitial, an Akamai sensor challenge — these need a JavaScript runtime. \u003Ca href=\"/blogs/bypass-datadome-web-scraping-2026\">DataDome in particular makes a browser mandatory\u003C/a>.\u003C/p>\n\u003Cp>\u003Cstrong>It does not give you proxies.\u003C/strong> Compare the datacentre and residential columns above; roughly 16–20 points of each row is the IP layer, and that is a separate purchase.\u003C/p>\n\u003Cp>\u003Cstrong>It does not address behavioural scoring.\u003C/strong> There is no mouse to move, which cuts both ways: nothing to get wrong, and nothing to offer a vendor that scores interaction. Request-timing distribution is the one behavioural signal you still own — randomise across 300–2,500ms rather than sleeping a fixed interval, because a metronome is a fingerprint.\u003C/p>\n\u003Ch2 id=\"outside-python\">Outside Python\u003C/h2>\n\u003Cp>\u003Ccode>curl_cffi\u003C/code> is Python-only, but the technique is not. This matters because the usual advice for a non-Python stack is &quot;use a stealth browser&quot;, and on Akamai that is the more expensive and less effective choice.\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>Go\u003C/strong> — \u003Ca href=\"https://github.com/refraction-networking/utls\">uTLS\u003C/a> exposes the \u003Ccode>ClientHello\u003C/code> for direct construction and has browser presets; \u003Ca href=\"https://github.com/bogdanfinn/tls-client\">tls-client\u003C/a> wraps it in a friendlier HTTP interface.\u003C/li>\n\u003Cli>\u003Cstrong>Any language\u003C/strong> — run the \u003Ccode>curl-impersonate\u003C/code> binary and shell out to it, or use its shared library through your FFI. Less elegant, works everywhere.\u003C/li>\n\u003Cli>\u003Cstrong>Node.js\u003C/strong> — bindings exist but churn; the binary route is the stable one.\u003C/li>\n\u003C/ul>\n\u003Cp>There is no \u003Ca href=\"/blogs/what-is-patchright-guide-2026\">Patchright\u003C/a> port for Go, Java or .NET, so on those stacks TLS impersonation is often the only socket-level option available to you.\u003C/p>\n\u003Ch2 id=\"where-this-fits-in-a-stack\">Where This Fits in a Stack\u003C/h2>\n\u003Cp>The useful habit is to ask the cheapest question first.\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>Is the data in the HTML?\u003C/strong> One request answers it. If yes, you may never need a browser.\u003C/li>\n\u003Cli>\u003Cstrong>Does \u003Ccode>requests\u003C/code> get blocked?\u003C/strong> Try \u003Ccode>impersonate=\u003C/code> before anything else. It is one argument.\u003C/li>\n\u003Cli>\u003Cstrong>Still blocked?\u003C/strong> Residential proxies. Usually 16–20 points.\u003C/li>\n\u003Cli>\u003Cstrong>Still blocked, and the page needs JavaScript?\u003C/strong> Now a patched browser is justified — see the \u003Ca href=\"/blogs/playwright-stealth-2026\">Playwright stealth benchmark\u003C/a>.\u003C/li>\n\u003Cli>\u003Cstrong>Challenge widget?\u003C/strong> A browser plus challenge handling, and a real cost conversation.\u003C/li>\n\u003C/ol>\n\u003Cp>Most teams enter at step 4 because that is what the tutorials cover, and carry a 200MB browser process per worker for pages that would have been a 50KB HTTP response.\u003C/p>\n\u003Ch2 id=\"the-maintenance-you-are-signing-up-for\">The Maintenance You Are Signing Up For\u003C/h2>\n\u003Cp>Impersonation is a moving target by construction. Browsers change their handshakes; the library has to follow; vendors update what they flag. Akamai alone ships sensor changes roughly monthly, and each one can move a pass rate by 15–30 points. Your pinned \u003Ccode>impersonate=&quot;chrome131&quot;\u003C/code> is accurate until Chrome 133 is the normal thing to see on the wire, at which point your perfect fingerprint is a conspicuously old one.\u003C/p>\n\u003Cp>Nothing about that cycle appears in \u003Ccode>pip install\u003C/code>, and it is the real cost of a self-hosted fetch layer.\u003C/p>\n\u003Cp>If what you need is \u003Cstrong>competitor prices, stock levels and product data arriving in your systems\u003C/strong> rather than a fingerprint stack to keep current, this is the wrong question to be answering. \u003Ca href=\"/\">ScrapeWise\u003C/a> runs the handshakes, the proxies and the challenge handling and delivers structured rows. No plan and no compute units: a balance you top up, and a published charge per delivered page — EUR 0.15 per 1,000 for plain requests, EUR 0.75 with rendering, EUR 1.50 through the residential tier and EUR 3.75 for render plus residential. New accounts get 5 free requests and the balance never expires.\u003C/p>\n\u003Cp>You can also call \u003Ca href=\"/scrapers\">ready-made scraping API endpoints\u003C/a> directly, or see how this feeds \u003Ca href=\"/use-cases/competitor-price-tracking\">competitor price tracking\u003C/a>.\u003C/p>\n\u003Ch2 id=\"so-should-you-use-curlcffi\">So Should You Use curl_cffi?\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Yes\u003C/strong> — \u003Ccode>requests\u003C/code> is blocked, the data is in the HTML, and you are on Python. Highest return per line of code in scraping.\u003C/li>\n\u003Cli>\u003Cstrong>Yes\u003C/strong> — your target is Akamai. 87% beats every browser we tested, for a quarter of the compute.\u003C/li>\n\u003Cli>\u003Cstrong>Maybe\u003C/strong> — standard Cloudflare at 79%. Try it first; it is cheap to find out.\u003C/li>\n\u003Cli>\u003Cstrong>No\u003C/strong> — the page is empty without JavaScript, or there is a challenge widget. Use \u003Ca href=\"/blogs/what-is-patchright-guide-2026\">a patched browser\u003C/a> or \u003Ca href=\"/blogs/camoufox-vs-patchright-2026\">Camoufox\u003C/a>.\u003C/li>\n\u003Cli>\u003Cstrong>Not the question\u003C/strong> — you want delivered rows on a schedule and would rather not keep a fingerprint current.\u003C/li>\n\u003C/ul>\n\u003Cp>\u003Ca href=\"/pricing\">See pricing\u003C/a>.\u003C/p>\n",{"title":11,"description":12,"badge":13,"benefits":14},"Frequently asked questions","What a TLS fingerprint is, how curl_cffi impersonates a browser handshake, measured pass rates per anti-bot vendor, the four things it cannot do, and the equivalents outside Python","FAQ",[15,18,21,24,27,30,33,36,39],{"title":16,"description":17},"What is a TLS fingerprint and why does it block my scraper?","When a client opens an HTTPS connection it sends a ClientHello listing the TLS versions, cipher suites, extensions, elliptic curves and ALPN protocols it supports. RFC 8446 specifies what may appear but not the exact set or the order, so every TLS implementation picks differently. Hash that set and you have a stable identifier for the library that made the connection — that is JA3, and its successor JA4. Python's requests, which sits on OpenSSL via urllib3, produces a hash that looks nothing like a browser's and is globally known. Critically, it is read before your request line, so your realistic User-Agent header has not even been transmitted yet when the decision is available. That is why setting a browser user-agent and still getting blocked is the most common confusion in scraping: you dressed the request, but the handshake had already introduced you.",{"title":19,"description":20},"How does curl_cffi work?","curl-impersonate is a build of curl patched to produce byte-identical TLS handshakes to specific real browser versions. Three things change. The TLS library is swapped — OpenSSL out, BoringSSL in for Chrome targets and NSS for Firefox targets, the libraries those browsers actually ship, because a fingerprint is a property of the implementation and cannot be faked convincingly from a different one. The ClientHello is reordered to the real browser's cipher and extension sequence. And the HTTP/2 fingerprint is matched too: header table size, initial window size and stream priority are set to the browser's values, which matters because vendors fingerprint the HTTP/2 SETTINGS frame as well as the handshake — a correct JA3 with default HTTP/2 settings still gets caught on the stricter vendors. curl_cffi is Python bindings over that build, with an API deliberately shaped like requests: pip install curl_cffi, then from curl_cffi import requests and pass impersonate=\"chrome\".",{"title":22,"description":23},"Can you set a custom JA3 fingerprint instead of using a preset?","Yes. The impersonate= presets cover the common cases, but if a target has flagged the exact Chrome build everyone impersonates you can specify the fingerprint directly: ja3= sets the TLS fingerprint, akamai= sets the HTTP/2 one, and extra_fp= covers the remaining knobs such as signature algorithms and certificate compression. Treat it as a last resort rather than a starting point. A hand-built fingerprint that does not correspond to any real browser is worse than a shared one, because \"nothing on earth produces this\" is an easier call for a vendor to make than \"one of 400 million Chrome users\". Before changing anything, check what you currently look like — tls.browserleaks.com/json returns your JA3 and JA4 hashes as JSON, so one request tells you whether the handshake is really your problem.",{"title":25,"description":26},"How much does TLS impersonation actually improve pass rates?","In our April 2026 tests of 500 requests per approach, the handshake alone was worth roughly 40 to 50 points on every vendor. On Akamai Bot Manager v4 the HTTP path went from 22% with plain requests on datacentre proxies to 71% with curl-cffi, and 87% on residential. On standard Cloudflare: 19% to 61% to 79%. On Cloudflare Enterprise: 4% to 38% to 52%. On PerimeterX: 11% to 29% to 41%. Read down a column rather than across a row and the point changes: the ceiling is set by the vendor, not by you, and 87% on Akamai and 41% on PerimeterX come from identical code.",{"title":28,"description":29},"Why does curl_cffi beat a stealth browser on Akamai?","Because Akamai ranks TLS fingerprinting as its primary detection signal rather than a secondary one, so fixing layer one fixes most of the problem. curl-cffi on residential proxies reached 87% against Bot Manager v4, higher than Patchright's 74% and Camoufox's 83%, at roughly a quarter of the compute. Cloudflare weights JavaScript challenges more heavily, which is why a browser catches up there. Note also that datacentre proxies cap every row: a perfect handshake from a pre-flagged ASN is still refused at the edge, and roughly 16 to 20 points of each result is the IP layer.",{"title":31,"description":32},"What can curl_cffi not do?","Four things. It runs no JavaScript, so if the price is written into the DOM by a client-side script you get the markup without it — check first by fetching the page and searching the raw HTML for a known value. It cannot solve a challenge widget: Turnstile, a DataDome interstitial or an Akamai sensor challenge all need a JavaScript runtime. It does not give you proxies, which is a separate purchase. And it does not address behavioural scoring, though request timing is the one behavioural signal you still own — randomise across 300 to 2,500ms rather than sleeping a fixed interval.",{"title":34,"description":35},"Is there a curl_cffi for Go, Node or .NET?","curl_cffi itself is Python-only, but the technique is not. In Go, uTLS exposes the ClientHello for direct construction and ships browser presets, and tls-client wraps it in a friendlier HTTP interface. In any language you can run the curl-impersonate binary and shell out to it, or use its shared library through your FFI — less elegant, works everywhere. Node bindings exist but churn, so the binary route is the stable one. This matters because there is no Patchright port for Go, Java or .NET, so on those stacks TLS impersonation is often the only socket-level option available.",{"title":37,"description":38},"Where should curl_cffi sit in a scraping stack?","Ask the cheapest question first. Is the data in the HTML? One request answers it, and if yes you may never need a browser. Does requests get blocked? Try impersonate= before anything else — it is one argument. Still blocked? Residential proxies, usually worth 16 to 20 points. Still blocked and the page needs JavaScript? Now a patched browser is justified. Challenge widget? A browser plus challenge handling, and a real cost conversation. Most teams enter at step four because that is what the tutorials cover, and carry a 200MB browser process per worker for pages that would have been a 50KB HTTP response.",{"title":40,"description":41},"What is the ongoing maintenance cost of TLS impersonation?","Impersonation is a moving target by construction. Browsers change their handshakes, the library has to follow, and vendors update what they flag. Akamai alone ships sensor changes roughly monthly and each one can move a pass rate by 15 to 30 points. A pinned impersonate=\"chrome131\" is accurate until a later Chrome is the normal thing to see on the wire, at which point your perfect fingerprint is a conspicuously old one. None of that cycle appears in pip install. If what you need is competitor prices, stock levels and product data arriving in your systems rather than a fingerprint stack to keep current, ScrapeWise runs the handshakes, proxies and challenge handling and delivers structured rows from EUR 0.15 per 1,000 delivered pages, with 5 free requests on every new account.","curl_cffi 2026: TLS Impersonation Without a Browser","Your requests call is blocked before any JavaScript runs, because its TLS handshake is not a browser's. What JA3/JA4 is, how curl_cffi fixes it, measured pass rates, and the four cases where it cannot help.","Siim Brazier","/img/team/siim.jpg",false,"Scraping",8,[50,55,60],{"slug":51,"title":52,"image":53,"date":5,"category":47,"excerpt":54},"playwright-akamai-bot-manager-2026","Playwright vs Akamai Bot Manager: Why the Browser Is the Wrong Layer (2026)","/img/news/playwright-akamai-bot-manager-2026.png","Vanilla Playwright does not clear Akamai Bot Manager v4. Patched Playwright reaches 74% on residential proxies. A plain HTTP client reaches 87% for a quarter of the compute. The measurements and the reason.",{"slug":56,"title":57,"image":58,"date":5,"category":47,"excerpt":59},"what-is-patchright-guide-2026","What Is Patchright? The Reference Guide (2026)","/img/news/what-is-patchright-guide-2026.png","Patchright is Playwright with the headless tells patched out of the Chromium build. What it patches, how to install it in Python and Node, and the four things it cannot do.",{"slug":61,"title":62,"image":63,"date":64,"category":47,"excerpt":65},"build-web-scraper-with-claude-mcp-2026","Build a Web Scraper with Claude and MCP: A Step-by-Step Walkthrough","/img/news/build-web-scraper-with-claude-mcp-2026.png","30 Sep 2026","Connect Claude to Scrapewise through MCP and let it build, test and run price scrapers for you. The setup, the prompts and the guardrails we use.",{"slug":51,"title":52},[68,72,75,78,82,85,88,91,94,97],{"level":69,"text":70,"id":71},2,"Choose Before You Read","choose-before-you-read",{"level":69,"text":73,"id":74},"What a TLS Fingerprint Actually Is","what-a-tls-fingerprint-actually-is",{"level":69,"text":76,"id":77},"What curl_cffi Does About It","what-curlcffi-does-about-it",{"level":79,"text":80,"id":81},3,"When a preset is not enough","when-a-preset-is-not-enough",{"level":69,"text":83,"id":84},"What It Is Worth, Measured","what-it-is-worth-measured",{"level":69,"text":86,"id":87},"The Four Things It Cannot Do","the-four-things-it-cannot-do",{"level":69,"text":89,"id":90},"Outside Python","outside-python",{"level":69,"text":92,"id":93},"Where This Fits in a Stack","where-this-fits-in-a-stack",{"level":69,"text":95,"id":96},"The Maintenance You Are Signing Up For","the-maintenance-you-are-signing-up-for",{"level":69,"text":98,"id":99},"So Should You Use curl_cffi?","so-should-you-use-curlcffi",[101,102,103,104,105],"A requests call is identifiable from its TLS handshake alone, before a byte of your page or a line of JavaScript runs.","curl_cffi replays a real browser's handshake at the socket level. On Akamai that took us from 22% to 87% with no browser process.","It is the strongest option on Akamai, decent on standard Cloudflare at 79%, and weak on PerimeterX at 41%.","It runs no JavaScript. If the data is injected client-side or gated behind a challenge widget, no handshake fixes that.","Check whether the data is already in the HTML before reaching for a browser. One curl is enough to find out.",1791887687144]