Guide

How to use curl-impersonate with proxies (HTTP, SOCKS5, curl_cffi)

Run curl-impersonate and curl_cffi through HTTP and SOCKS5 proxies, with passwords, and prove the target still sees a real Chrome fingerprint.

HProxy Team··Updated September 2, 2026·19 min read
HProxy.Guide

Free proxies won't hold up here.

Shared datacenter IPs get flagged and dropped fast. When it has to hold, gaming, streaming, accounts, you need mobile and residential IPs that read as a real device, from $0.44/GB, pay as you go.

Proxies for Web Scraping

Put the proxy in -x, exactly as with plain curl, and keep the wrapper script: curl_chrome150 -x http://user:pass@host:port https://tls.peet.ws/api/all. In Python it is one call with curl_cffi: requests.get(url, impersonate="chrome", proxy="http://user:pass@host:port"). The proxy changes the address the target sees. The wrapper changes the TLS and HTTP/2 handshake the target sees. We measured both on 2 September 2026. Through three public HTTP proxies and a SOCKS5 proxy, curl_chrome150 kept the JA4 of a real Chrome 151. Only the address changed.

Everything below comes from that run. The tools were curl-impersonate 2.2.2 with curl 8.21.0 inside, curl_cffi 0.16.3 and a fingerprint echo service. The proxies were public entries from our own list, plus two local test proxies for the password cases. Where a step needs a live proxy, pull one from our free proxy API.

Why does a proxy alone not fix a blocked request?

Every TLS connection starts with a ClientHello. RFC 8446 requires it as the first message. It lists the cipher suites and extensions the client supports, in the client's order, before any encryption starts. JA3, the method Salesforce published in 2017, hashes the TLS version, the ciphers, the extensions, the curves and the point formats of that message into one MD5. JA4, by FoxIO, sorts the extensions first. Cloudflare's bot documentation says the resulting fingerprint "acts as a stable identifier across different destination IPs, ports, and certificates".

HTTP/2 adds a second signature. RFC 9113 makes both sides send a SETTINGS frame at the start of a connection. The values are not negotiated; they describe the sender. The order of the settings, the window update and the pseudo-headers is stable per client. A proxy touches neither layer. RFC 9110 defines CONNECT as a tunnel that restricts the proxy "to blind forwarding of data, in both directions". A SOCKS proxy relays bytes the same way. The address changes, the handshake does not.

The table shows what the echo service saw from each client on 2 September 2026, without a proxy.

clientJA4HTTP/2 hashJA3reads as
curl 8.5.0t13d3112h2_e8f1e7e78f70_b26ce05bbdd664a832f5stablecurl
Python urllib, OpenSSL 3.0t13d1813h1_85036bcba153_d339722ba4afnone (HTTP/1.1)stablea script
curl-impersonate, no wrappert13d2713h2_b4f9224df7c1_91b187c34d4052d84b11stablecurl
curl_chrome150 wrappert13d1516h2_8daaf6152771_806a8c22fdea52d84b11new per connectionChrome
curl_cffi impersonate="chrome"t13d1516h2_8daaf6152771_806a8c22fdea52d84b11new per connectionChrome
real Chrome 151, Windowst13d1516h2_8daaf6152771_806a8c22fdea52d84b11new per connectionChrome
curl_firefox147 wrappert13d1717h2_5b57614c22b0_3cbfd9057e0d6ea73faastableFirefox

The wrapper and the Python binding matched the real browser on JA4, on the HTTP/2 fingerprint and on the echo service's own hash. The JA3 column is the one people trip over. Chrome has randomised the order of its ClientHello extensions since version 110, per the Chrome Platform Status entry. Every Chrome-target connection in our run produced a new JA3 while JA4 stayed constant. The curl_cffi FAQ says the same: the JA3 of the binding and of a browser "may differ", by design. What is JA3 and JA4 fingerprinting has the full method. How websites detect proxies covers the other signals.

Two signals have to pass: the fingerprint and the address
  1. Raw curl or Python

    script handshake

  2. curl-impersonate + proxy

    browser JA4 + the proxy's address

  3. Anti-bot

    both signals pass

  4. Target

    serves the page

Source: Fingerprint and address are separate checks

Which curl-impersonate should I install?

There are two projects with the same name. The original, by lwthiker, published its last release, 0.6.1, on 2 March 2024. It sits on curl 8.1.1, its newest Chrome is 116, and it needs two binaries plus the NSS packages. The fork by lexiforest, which also maintains curl_cffi, calls itself "an active fork". It published 2.2.2 on 1 September 2026 on curl 8.21.0, as one binary for Chrome, Edge, Safari, Firefox and Tor. Tutorials that tell you to install libnss3 or to pull a 0.5 or 0.6 Docker tag describe the old project.

pathwhat you getproxy goes inplatformssize and version
release tarballcurl-impersonate binary plus 38 wrapper scripts-x on the wrapperLinux (glibc and musl, x86_64, arm, riscv64), macOS, Windows, FreeBSD, Android, iOS10.8 MB download, 30.9 MB binary, v2.2.2 of 1 September 2026
Docker imagethe same binary and wrappers in a container-x after the wrapper name in docker runanywhere Docker runslexiforest/curl-impersonate 58 MB, :alpine 32 MB, tags latest, alpine and version numbers, updated 1 September 2026
curl_cffilibcurl-impersonate as a Python package with a requests-style API and a curl-cffi commandproxy= or proxies= in the callLinux, macOS, Windows wheels, Python 3.10 or newer0.16.3 of 2 September 2026

The release binary

Download the tarball for your platform from the fork's releases page on GitHub and unpack it. The docs say the prebuilt binaries "only rely on the target system's standard runtime libraries" and may need the ca-certificates package. On Ubuntu 24.04 we used the x86_64-linux-gnu build.

curl -sSLO https://github.com/lexiforest/curl-impersonate/releases/download/v2.2.2/curl-impersonate-v2.2.2.x86_64-linux-gnu.tar.gz
mkdir curl-impersonate && tar -xzf curl-impersonate-v2.2.2.x86_64-linux-gnu.tar.gz -C curl-impersonate
cd curl-impersonate && ./curl-impersonate --version | head -1
curl 8.21.0-IMPERSONATE (Linux) libcurl/8.21.0-IMPERSONATE BoringSSL zlib/1.3.1 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.7 nghttp2/1.63.0 ngtcp2/1.20.0 nghttp3/1.15.0

The folder holds 38 wrappers. They cover 17 desktop Chrome versions from 99 to 150, two Android Chrome, two Edge, eleven Safari including iOS, four Firefox up to 147, and Tor 14.5. The newest wrappers are one line, curl-impersonate --compressed --impersonate chrome150, because the fingerprint now lives inside the binary. Older ones such as curl_chrome136 spell out the ciphers, the curves, fourteen headers and the HTTP/2 settings. Reading one is a good way to see what a browser signature consists of. On macOS the docs give brew install lexiforest/tap/curl-impersonate, on Arch pacman -S curl-impersonate.

The Windows tarball ships curl-impersonate.exe and 36 .bat wrappers. It has no curl_chrome150.bat. In our run curl-impersonate.exe --compressed --impersonate chrome150 still gave the Chrome 150 JA4, and curl_chrome146.bat worked direct and through a proxy.

The Docker image

The fork's images live under lexiforest/curl-impersonate on Docker Hub. The tags are latest, alpine and version numbers such as v2.2.2; there is no tag named after a browser. The docs' example, with the proxy flag added after the wrapper name, reads as follows.

docker run --rm lexiforest/curl-impersonate:v2.2.2 curl_chrome150 -x http://user:pass@host:port https://tls.peet.ws/api/all

We read the tag list from the registry but did not run the image. Treat the line as the documented form rather than a measured one.

curl_cffi for Python

pip install curl_cffi --upgrade
curl-cffi list

The curl_cffi README states Python 3.10 as the minimum since version 0.14. It promises the install works "on Linux, macOS and Windows out of the box". curl-cffi list printed the same 38 targets as the Linux wrappers on our 0.16.3 install. curl-cffi update fetches new presets without a new package.

How do I run the wrapper through an HTTP proxy?

Step 1: use a wrapper, never the bare binary

The docs are explicit: "running the binary directly does not produce the same TLS and HTTP/2 signatures as the impersonated browsers". We measured it. The bare binary reported JA4 t13d2713h2_b4f9224df7c1_91b187c34d40 and the user agent curl/8.21.0-IMPERSONATE. curl_chrome150 gave the Chrome value. Pick the wrapper that matches the headers you send.

Step 2: add -x, with the password in the URL or in -U

The wrapper passes every flag to curl. The proxy syntax is therefore the one from our cURL proxy guide.

# proxy without a password
curl_chrome150 -x http://203.0.113.7:8080 https://tls.peet.ws/api/all

# password in the URL; @ and : inside the password become %40 and %3a
curl_chrome150 -x http://user:pa%40ss@203.0.113.7:8080 https://tls.peet.ws/api/all

# password in -U, which curl hides from process listings where it can
curl_chrome150 -x http://203.0.113.7:8080 -U user:pass https://tls.peet.ws/api/all

The curl manual sets the rules. A proxy string with no scheme or with http:// is an HTTP proxy. The port defaults to 1080 when omitted. -x overrides the https_proxy environment variable. The user and password in the string are URL-decoded. All three forms returned HTTP 200 through our local test proxy.

Step 3: read what the target saw

Send both a plain curl and the wrapper through the same proxy and compare. This capture is our own run through a public entry from our list.

Terminal capture of the run: plain curl and curl_chrome150 through the same public HTTP proxy, then a timeout and a 407 message.
Four commands from our run on 2 September 2026. Same proxy, same exit address. Plain curl keeps its own JA4; curl_chrome150 sends the JA4 of a real Chrome 151. The last two lines are the two most common failures.
fp() { python3 -c "import json,sys;d=json.load(sys.stdin);print(d['ip'], d['tls']['ja4'], d['http2']['akamai_fingerprint_hash'])"; }
curl -s -x http://103.237.102.191:11111 https://tls.peet.ws/api/all | fp
curl_chrome150 -s -x http://103.237.102.191:11111 https://tls.peet.ws/api/all | fp
103.237.102.191:48060 t13d3112h2_e8f1e7e78f70_b26ce05bbdd6 64a832f547be33249bf4d33e8a46c5dc
103.237.102.191:48072 t13d1516h2_8daaf6152771_806a8c22fdea 52d84b11737d980aef856699f885ca86

We repeated the wrapper run through three public HTTP proxies, a local HTTP proxy with a password and a local SOCKS5 proxy.

route for curl_chrome150exit address seenJA4HTTP/2 hash
directourst13d1516h2_8daaf6152771_806a8c22fdea52d84b11
public HTTP proxy 103.237.102.191the proxysamesame
public HTTP proxy 116.202.172.187the proxysamesame
public HTTP proxy 87.251.77.29the proxysamesame
local HTTP proxy with Basic authourssamesame
local SOCKS5 proxy with a passwordourssamesame
public SOCKS5 entry that terminates TLSno page: exit 60not oursnot ours

Every route negotiated HTTP/2 through the proxy. The wrapper's values never moved. The last row is the one to remember, and it gets its own section below.

Step 4: add nothing that touches TLS

The docs list the flags that change the signature: --ciphers, --curves, --no-npn, --no-alpn, --tls-max, --tls13-ciphers and every --tlsv1.x option. We added --tlsv1.3 to the chrome150 wrapper. The JA4 became t13d312h2_55b375c5d22e_6f1636e650ef. Proxy flags, -H for extra headers and -o are safe; TLS flags are not.

How do I use a SOCKS5 proxy with curl-impersonate?

Use the socks5h:// scheme so the proxy resolves the hostname. With socks5://, curl resolves the name on your machine first and sends the address. The manual describes --socks5 as "resolve the hostname locally" and --socks5-hostname as "let the proxy resolve the hostname". RFC 1928 gives the SOCKS5 request a domain-name address type for exactly this. In our run -v printed Host httpbin.org:443 was resolved. before the SOCKS connect with socks5://. With socks5h:// no such line appeared.

curl_chrome150 -x socks5h://user:pass@203.0.113.7:1080 https://tls.peet.ws/api/all
curl_chrome150 -x socks5h://203.0.113.7:1080 --proxy-user user:pass https://tls.peet.ws/api/all

Both forms returned 200 through our local SOCKS5 server. A wrong password came back in 0.02 seconds as curl: (97) User was rejected by the SOCKS5 server (1 1). No password at all gave curl: (97) No authentication method was acceptable. For a SOCKS hop in front of an HTTP proxy, the manual offers --preproxy socks5://host:1080 together with -x.

HTTP proxySOCKS5 proxy
scheme in -xhttp:// (the default)socks5h://, or socks5:// for local DNS
how HTTPS passesCONNECT tunnel, then blind forwardingrelay after the SOCKS handshake
who resolves the namethe proxysocks5h: the proxy; socks5: you
passwordURL or -U, Basic over CONNECTURL or --proxy-user, SOCKS username method
wrong passwordCONNECT tunnel failed, response 407 (or 401)(97) User was rejected by the SOCKS5 server
scheme pointed at the wrong port(56) Proxy CONNECT aborted at a SOCKS port(28) timeout at an HTTP port
fingerprintunchangedunchanged

One warning about public SOCKS5 lists, ours included. On 2 September 2026 all 40 recent SOCKS5 entries we pulled from our list completed the SOCKS handshake and then presented a certificate that curl refused. Read on before you reach for -k.

How do I use a proxy with curl_cffi in Python?

The binding wraps the same library, so the proxy syntax is curl's. The docs prefer the single proxy argument. They also warn that "a very common mistake is to add https:// prefix to the https proxy". A requests-style proxies dict and the http_proxy and https_proxy variables work as well.

from curl_cffi import requests

r = requests.get(
    "https://tls.peet.ws/api/all",
    impersonate="chrome",
    proxy="http://user:pass@203.0.113.7:8080",
)
d = r.json()
print(d["ip"], d["tls"]["ja4"], d["http2"]["akamai_fingerprint_hash"])

impersonate="chrome" selects the newest Chrome preset, which was Chrome 150 in 0.16.3; impersonate="chrome136" pins one. Through two public proxies this call returned the proxy's address with the same JA4 and HTTP/2 hash as the direct run. Both the proxy= and the proxies= form did. A SOCKS5 proxy is the same argument with socks5h://.

Keep the fingerprint and the proxy across requests

A Session holds the target, the proxy and the cookies, so a login flow keeps one identity.

from curl_cffi import requests

s = requests.Session(impersonate="chrome", proxy="socks5h://user:pass@203.0.113.7:1080")
s.get("https://example.com/login")
r = s.get("https://example.com/dashboard")

For concurrency the AsyncSession takes the same two arguments. The README lists "asyncio with proxy rotation on each request" as a feature: pass proxy= per call inside the session.

import asyncio
from curl_cffi import AsyncSession

async def main():
    async with AsyncSession(impersonate="chrome") as s:
        r = await s.get("https://tls.peet.ws/api/all", proxy="http://user:pass@203.0.113.7:8080")
        print(r.json()["ip"])

asyncio.run(main())

Rotating proxies by username

Many pools rotate the exit when the username changes and nothing else does. Issues #294 and #308 on the curl_cffi tracker reported that a session kept the first exit for every request in that setup. The cause sits in libcurl. It reused the connection and the TLS session because the host and port had not changed. The fork added --proxy-credential-no-reuse in a pull request merged on 5 July 2025. curl_cffi enabled the matching proxy_credential_no_reuse option in 0.12, so the session cache is bound to the proxy username. The impersonate guide adds a second reason. A server may tie a TLS session ticket to the address that earned it, so resuming it from another exit looks wrong. Update the package, or open one Session per proxy.

The same engine plugs into a crawler as a download handler; proxies with Scrapy shows that setup.

Does the proxy change the fingerprint?

Not a proxy that does its job. RFC 9110 restricts a CONNECT proxy to blind forwarding, and a SOCKS5 proxy relays bytes. Our table above shows the same JA4 and HTTP/2 hash on every working route. A proxy that terminates TLS is a different animal. It opens its own connection to the target, so the target sees the proxy's handshake. It also presents its own certificate to you.

The public SOCKS5 entries on our list did exactly that on 2 September 2026. All 40 completed the SOCKS handshake. A plain http:// request through three of them returned the entry's own address. Every HTTPS request ended in curl: (60) SSL certificate problem: unable to get local issuer certificate. With -v the certificate for httpbin.org carried the subject CN=httpbin.org, O=Parallels and the issuer CN=Parallels Panel operations. That is a web-panel certificate forged for the hostname we asked for. curl_cffi raised CertificateVerifyError with the same names. Issue #417 on the curl_cffi tracker records the paid version of the same problem. A proxy manager with TLS interception switched on removed the Akamai hash from the request; turning the interception off brought it back.

The certificate error is therefore the useful signal. Do not answer it with -k or verify=False. The request would go through, the exit would read everything inside it, and the target would see the proxy's fingerprint instead of Chrome's. Use another proxy.

What do the proxy errors mean?

Every message below is one we produced on 2 September 2026, with curl 8.21.0 inside curl-impersonate 2.2.2 and curl_cffi 0.16.3. One version note first. A failed CONNECT was exit 56 on curl 8.5.0 and is exit 7 on curl 8.21.0, with the same text.

messageexitcurl_cffi raiseswhat happenedfix
CONNECT tunnel failed, response 4077 (56 on 8.5.0)ConnectionErrorthe proxy wants a passwordadd user:pass or -U; see fixing 407
CONNECT tunnel failed, response 4017ConnectionErrorsome proxies answer a wrong password with 401check the credentials
CONNECT tunnel failed, response 400, 403, 502, 5037 (56 on 8.5.0)ConnectionErrorthe proxy refuses to tunnel to that host or portthe proxy, not the wrapper; try another entry
A libcurl function was given a bad argument43ValueError, not a curl_cffi class, in 0.16.3the proxy answered CONNECT with HTML and no status lineHTTP-only forward proxy; use it for http:// targets only
Connection timed out after N milliseconds28Timeoutdead proxy, or a SOCKS scheme sent to an HTTP portset --connect-timeout, check the scheme
Proxy CONNECT aborted56ConnectionErrorhttp:// scheme sent to a SOCKS portuse socks5h://
TLS connect error: WRONG_VERSION_NUMBER35SSLErrorhttps:// scheme on a plain HTTP proxywrite http://
Unsupported proxy scheme7ConnectionErrora typo in the schemehttp, socks4, socks4a, socks5 or socks5h
Failed to connect to host over proxy7ConnectionErrornothing listens on that portcheck host and port
User was rejected by the SOCKS5 server (1 1)97ProxyErrorwrong SOCKS5 passwordcheck the credentials
No authentication method was acceptable97ProxyErrorthe SOCKS5 server requires a passwordadd user:pass
SSL certificate problem: unable to get local issuer certificate60CertificateVerifyErrorthe proxy terminates TLS itselfanother proxy, never -k

Two of these rows carry numbers from the probe. Of 40 recent public HTTP entries on our list, 11 carried an HTTPS request end to end. Twenty refused the CONNECT with one of the codes in row three, four answered with the bare HTML of row four, four timed out and one presented a self-signed certificate. Plain curl got the same answers as the wrapper. That comparison is the quickest way to tell a proxy fault from an impersonation fault. The general curl messages, including the DNS ones, are in how to fix curl proxy errors.

How do I verify both signals?

Check the address and the fingerprint in one request, against an endpoint that echoes both. The fp function from the capture prints the three values that matter: the exit address, the JA4 and the HTTP/2 hash.

fp() { python3 -c "import json,sys;d=json.load(sys.stdin);print(d['ip'], d['tls']['ja4'], d['http2']['akamai_fingerprint_hash'])"; }
curl_chrome150 -s -x http://user:pass@203.0.113.7:8080 https://tls.peet.ws/api/all | fp

Three things to compare. The address must be the proxy's. The JA4 must equal the one your real browser shows on the same page. On 2 September 2026 Chrome 151 on Windows gave t13d1516h2_8daaf6152771_806a8c22fdea, the same as the chrome150 target. The JA3 will differ from your browser's and from your last run, because of the extension shuffle described above, so do not use it as the test. The HTTP/2 hash must be the browser's too. The bare binary already had Chrome's HTTP/2 hash with the wrong TLS, which is why both values need a look.

For the address side alone, our proxy checker reports the exit address, the anonymity grade, the real location and the latency in one paste. The free proxy list is the right pool for proving a setup. The numbers above say what to expect from it: 11 of 40 HTTP entries and none of the SOCKS5 entries carried a browser handshake that afternoon. For work that has to keep running, the address side has to be as steady as the handshake side. That is what our residential IPs are for.

What curl-impersonate cannot do

  • JavaScript. It patches TLS and HTTP handshakes and runs nothing above them. A challenge page stays unsolved. The curl_cffi FAQ says that for higher protection levels "you may need to find a better proxy IP provider and use browser automation tools like playwright". How to scrape past Cloudflare covers that step.
  • A stale target. The fork added chrome150 on 8 August 2026 and announced a chrome152 preset in the 0.16.3 notes. An old preset is a fingerprint of its own. Run curl-cffi update, or download the next release.
  • Edge, Brave and other Chromium browsers. The docs note that Chromium browsers share one fingerprint. They differ only in the User-Agent and sec-ch-ua-platform headers, so impersonate Chrome and set the headers yourself.
  • A TLS-terminating proxy. See above. The certificate error is the signal.
  • Lower layers. A Hacker News reader asked in April 2025 whether the tool also sets the IP time-to-live of the platform it imitates. We did not measure the IP layer. The tool's own claim covers TLS and HTTP only.
  • HTTP/3 through a proxy. The binding lists HTTP/3 with a UDP proxy since 0.15.0. We did not test it, and an August 2026 issue reports it failing in the 2.x line.

Where to go from here

Detection happens in layers. Clear the address with a proxy, the handshake with a wrapper or curl_cffi, and the behaviour with pacing and, where a challenge appears, a real browser. This guide is the middle layer, and the check at the end proves it holds through the proxy. From here, how websites detect proxies maps every signal a defender reads. Proxies for web scraping covers choosing the proxy type for a target. The cURL guide is the reference for every proxy flag the wrapper inherits. When a project graduates to production against real defences, get addresses nobody else is burning on our paid pools. They can be ordered and rotated from code, which is what the developer reference is for.

Sources

  • curl-impersonate README, lexiforest fork. GitHub, file last changed 8 August 2026. The fork's enhancements, the browser table, the wrapper scripts, the warning about flags that change the signature. github.com/lexiforest/curl-impersonate
  • curl-impersonate releases, lexiforest. GitHub. v2.2.2 on 1 September 2026; v2.1.0 (chrome150) on 8 August 2026; v2.0.0 (curl 8.21.0, CMake) on 1 August 2026.
  • curl-impersonate documentation: Quick Start, Installation, Impersonation and Fingerprints, API reference. lexiforest, Read the Docs, 2026. The bare binary, the flags that affect the TLS signature, the install paths, the target list, CURLOPT_PROXY_CREDENTIAL_NO_REUSE.
  • Pull request #154, Add new option --proxy-credential-no-reuse. lexiforest/curl-impersonate, merged 5 July 2025.
  • curl-impersonate README and releases, lwthiker. GitHub. Last release v0.6.1 on 2 March 2024; v0.6.0 on curl 8.1.1.
  • Docker Hub tag lists for lexiforest/curl-impersonate and lwthiker/curl-impersonate, read 2 September 2026.
  • curl_cffi README and releases, lexiforest. GitHub. v0.16.3 on 2 September 2026. Python 3.10 minimum since v0.14, proxies, Session, AsyncSession, the curl-cffi command. github.com/lexiforest/curl_cffi
  • curl_cffi documentation: Advanced Topics (proxies), FAQ, Impersonate guide (PSK and proxy rotation). lexiforest, Read the Docs, 2026.
  • curl manual pages for --proxy, --proxy-user, --socks5, --socks5-hostname, --preproxy and --proxytunnel. The curl project, curl 8.21.0.
  • RFC 9110, HTTP Semantics, sections 9.3.6 CONNECT and 15.5.8 407 Proxy Authentication Required. IETF, June 2022.
  • RFC 8446, The Transport Layer Security (TLS) Protocol Version 1.3, section 4.1.2 Client Hello. IETF, August 2018.
  • RFC 1928, SOCKS Protocol Version 5. IETF, March 1996. RFC 9113, HTTP/2, section 6.5 SETTINGS. IETF, June 2022.
  • JA3, A method for profiling SSL/TLS Clients. Salesforce, GitHub, 2017. JA4+ Network Fingerprinting. FoxIO, GitHub.
  • JA3/JA4 fingerprint, Cloudflare Bots documentation. Cloudflare, last updated 6 May 2026.
  • TLS ClientHello extension permutation, feature 5124606246518784. Chrome Platform Status, shipped in Chrome 110.
  • Our own run of 2 September 2026: curl-impersonate 2.2.2 (Linux and Windows builds), curl_cffi 0.16.3, plain curl 8.5.0, Python urllib, Chrome 151, tinyproxy 1.11.1 and dante 1.4.3 as local test proxies, and 80 public entries from hproxy.com/free-proxy-list, against tls.peet.ws and tls.browserleaks.com. Raw output is kept in the page's research folder.

Frequently asked questions

How do I use a proxy with curl_cffi?
Pass proxy="http://user:pass@host:port" in the same call as impersonate="chrome". A requests-style proxies dict works too, and so do the http_proxy and https_proxy variables. Session and AsyncSession take the same arguments. Do not write https:// in front of a plain HTTP proxy; that produces the WRONG_VERSION_NUMBER error.
How do I install curl-impersonate?
Download the release tarball for your platform from the lexiforest fork on GitHub, unpack it, and run a wrapper such as curl_chrome150. Linux needs only the ca-certificates package. On macOS the docs give brew install lexiforest/tap/curl-impersonate, on Arch pacman -S curl-impersonate. For Python, pip install curl_cffi on Python 3.10 or newer.
Does curl-impersonate work on Windows?
Yes. Version 2.2.2 ships an x86_64-win32 tarball with curl-impersonate.exe and 36 .bat wrappers. In our run curl_chrome146.bat and curl-impersonate.exe --impersonate chrome150 both gave the Chrome JA4, direct and through a proxy. curl_cffi installs on Windows with pip.
Does a proxy change the TLS fingerprint?
A normal HTTP proxy tunnels HTTPS with CONNECT and a SOCKS5 proxy relays bytes, so neither can see or alter the handshake. In our run the JA4 and the HTTP/2 fingerprint were identical direct and through five proxies. A proxy that terminates TLS itself is the exception: it presents its own certificate, curl refuses with exit 60, and the target would see the proxy's handshake, not yours.
Why is the JA3 hash different from my browser's?
Chrome randomises the order of ClientHello extensions since version 110, and JA3 hashes the extension list in order, so each connection gets a new JA3. Compare JA4 instead, which sorts the extensions. In our run seven Chrome-target connections gave seven JA3 hashes and one JA4, and that JA4 matched a real Chrome 151.
Why does curl_cffi say CONNECT tunnel failed, response 400?
The proxy refused the CONNECT request that carries an HTTPS target. In our probe 20 of 40 public HTTP entries answered CONNECT with 400, 403, 502, 503, 404 or 500 for plain curl as well, so the proxy is the cause, not the impersonation. Test the same entry with plain curl, then try another entry. Older curl reports this as exit 56, curl 8.21.0 as exit 7.
Why does the proxy not rotate inside a curl_cffi Session?
Before curl_cffi 0.12 libcurl reused the connection and the TLS session when only the username changed, so every request left through the first exit. Since 0.12 the proxy_credential_no_reuse option binds both to the proxy credentials, and the curl-impersonate binary has the matching --proxy-credential-no-reuse flag. Update, or open one Session per proxy.
Can I use a SOCKS5 proxy in a curl_cffi Session?
Yes. Use proxy="socks5h://user:pass@host:1080" so the proxy resolves hostnames, or the proxies dict with the same value for http and https. A wrong password comes back as a ProxyError carrying curl error 97, User was rejected by the SOCKS5 server.
Does curl_cffi run JavaScript?
No. It reproduces the TLS and HTTP/2 handshake and nothing above it. A challenge page that hands the client a script to run is not solved by curl-impersonate or curl_cffi; the maintainer's own FAQ points to browser automation and a better proxy provider for those targets.

Proxies that don't die mid-job

Residential, ISP, datacenter and mobile, verified by the same engine that runs tens of millions of checks. They read as a real device and hold up under load. Pay as you go, and your balance never expires. $0.44/GB is the 2,000 GB+ rate; a single gigabyte is $0.50/GB, with no minimum order.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed