Markdown source
TLS Impersonation Libraries: curl_cffi, utls, wreq Markdown source
Readable source view for humans. The raw Markdown endpoint remains available for crawlers and agent readers.
---
title: "TLS Impersonation Libraries: curl_cffi, utls, wreq"
description: "TLS impersonation libraries compared — curl_cffi, wreq, utls, CycleTLS, curl-impersonate: which layers each replays, where they break, which to pick."
kind: article
maturity: budding
confidence: medium
origin: ai-drafted
author: "Agent"
directedBy: "krow"
tags: [tls, ja4, fingerprinting, scraping, python, rust]
published: 2026-05-20
modified: 2026-06-13
wordCount: 1447
readingTime: 7
prerequisites: [bot-detection-2026]
related: [bot-detection-2026, ja4-vs-ja3, ja4-fingerprint-t13d1516h2]
url: https://krowdev.com/article/tls-impersonation-library-comparison/
---
## Agent Context
- Canonical: https://krowdev.com/article/tls-impersonation-library-comparison/
- Markdown: https://krowdev.com/article/tls-impersonation-library-comparison.md
- Full corpus: https://krowdev.com/llms-full.txt
- Kind: article
- Maturity: budding
- Confidence: medium
- Origin: ai-drafted
- Author: Agent
- Directed by: krow
- Published: 2026-05-20
- Modified: 2026-06-13
- Words: 1447 (7 min read)
- Tags: tls, ja4, fingerprinting, scraping, python, rust
- Prerequisites: bot-detection-2026
- Related: bot-detection-2026, ja4-vs-ja3, ja4-fingerprint-t13d1516h2
- Content map:
- h2: Quick Reference: capability matrix
- h2: The cheap triage before reaching for a library
- h2: When each library wins
- h2: What none of them solve
- h2: Sources
- Crawl policy: same canonical content is exposed through HTML, Markdown, and llms-full; no crawler-specific content gate.
TLS impersonation libraries replay a real browser's first few hundred bytes — ClientHello, HTTP/2 SETTINGS, header order — so the wire shape your script emits matches a fingerprint the detection vendor already trusts from millions of real users. TLS impersonation is necessary, not sufficient. The same lib that gives you the right JA4 will still get IP-rep-blocked at a major CDN edge — here's the cheap test to run first, then the library matrix.
## Quick Reference: capability matrix
State verified live 2026-05-20: GitHub release tags + default-branch `pushed_at`. The single biggest correction versus older write-ups: the original `lwthiker/curl-impersonate` is **unmaintained**, and the active project is `lexiforest/curl-impersonate`. Anyone landing on the 6k-star lwthiker repo first picks a 2024-frozen toolchain.
| Library | Lang | JA3 | JA4 | HTTP/2 fp | Header order | Real BoringSSL/NSS | Maint | Last release |
|---|---|---|---|---|---|---|---|---|
| [lexiforest/curl_cffi](https://github.com/lexiforest/curl_cffi) | Python (cffi → libcurl-impersonate) | ✅ | ✅ | ✅ | ✅ | ✅ | active | v0.15.1b1 (2026-04-23) |
| [lexiforest/curl-impersonate](https://github.com/lexiforest/curl-impersonate) | C / curl fork (active) | ✅ | ✅ | ✅ | ✅ | ✅ | active | v1.5.6 (2026-05-02); v2.0.0a1 prerelease 2026-05-15 in progress |
| [lwthiker/curl-impersonate](https://github.com/lwthiker/curl-impersonate) | C / curl fork (original) | ✅ | ✅ | ✅ | ✅ | ✅ | **unmaintained since 2024** | v0.6.1 (2024-03-02) |
| [0x676e67/wreq](https://github.com/0x676e67/wreq) | Rust | ✅ | ✅ | ✅ | ✅ | ✅ (BoringSSL fork) | active | v6.0.0-rc.28 (2026-02-11) |
| [0x676e67/wreq-python](https://github.com/0x676e67/wreq-python) (formerly `rnet`) | Python (PyO3 → wreq) | ✅ | ✅ | ✅ | ✅ | ✅ | active | tracks wreq |
| [refraction-networking/utls](https://github.com/refraction-networking/utls) | Go (`crypto/tls` drop-in) | ✅ | partial (TLS only) | manual (your `x/net/http2`) | manual | Go `crypto/tls` fork that mimics browser ClientHellos (Chrome, Firefox, Edge, Safari) | active | v1.8.2 (2026-01-13) |
| [Danny-Dasilva/CycleTLS](https://github.com/Danny-Dasilva/CycleTLS) | Node + Go subprocess | ✅ | partial (preset-dependent) | weak | partial | uses utls | partial maint | cycletls/v2.0.3 |
| [FoxIO-LLC/ja4](https://github.com/FoxIO-LLC/ja4) | spec + Rust/Go/Python tooling | n/a | ✅ (defines JA4) | ✅ (JA4H/S/T family) | n/a | n/a | active | v0.18.8 (2025-11-19) |
| [salesforce/ja3](https://github.com/salesforce/ja3) | spec + Python | ✅ | — | — | — | n/a | **archived 2025-05** | — |
Two follow-ons that bite if you skim:
- `https://github.com/0x676e67/rnet` 301-redirects to `wreq-python`. The project was renamed — update bookmarks and any internal docs that still say "rnet".
- `ja4db.com` was unresponsive from at least one VPS region on 2026-05-20. The canonical JA4 DB is mirrored inside `FoxIO-LLC/ja4` and the Wireshark plugins; do not put `ja4db.com` in a CI lookup path.
Read down the row for the library, across for the capability gaps. The JA4 / HTTP/2 / header-order trio is the gate; "Real BoringSSL/NSS" is *why* a row has ticks in the first three; "maint" decides whether the row will still be honest in six months.
## The cheap triage before reaching for a library
Before you compare libraries, confirm you're actually being blocked by TLS — not by IP reputation, ASN classification, or a header coherence rule that no impersonation library can fix. The operating lesson from running this in production: **same 403 + same body before and after impersonation → IP rep, not TLS.** Swapping libraries does nothing.
The probe that anchors this is one function, hitting `tls.peet.ws` to see the JA4 your client actually emits:
```python
def verify_cffi_ja4(impersonate: str = "chrome131") -> str:
"""Hit tls.peet.ws and return the observed JA4. Use to confirm the
impersonation actually applied before running the experiment."""
from curl_cffi import requests as cffi_req
r = cffi_req.get("https://tls.peet.ws/api/all", impersonate=impersonate,
timeout=15)
return r.json().get("tls", {}).get("ja4", "")
# Real Chrome 131 JA4 starts with t13d1516h2_ (shared across modern
# Chromium-family browsers: Chrome 110+, recent Edge, Brave — see
# https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md)
ja4 = verify_cffi_ja4("chrome131")
assert ja4.startswith("t13d1516h2_"), f"impersonation did not apply: {ja4}"
```
The decoded prefix (`t13d1516h2`) is covered in the [JA4 t13d1516h2 snippet](/snippet/ja4-fingerprint-t13d1516h2/); the JA3-vs-JA4 background is in [JA3 vs JA4](/article/ja4-vs-ja3/). The same `tls.peet.ws` response also carries the Akamai HTTP/2 fingerprint hash under `http2.akamai_fingerprint_hash`, which is the cheapest way to confirm your HTTP/2 SETTINGS + pseudo-header order replay matches the impersonated browser without standing up your own H2 dissector. Once that probe asserts cleanly, hit the target both with stock `requests` and with `curl_cffi impersonate="chrome131"`. Two outcomes:
- **403 changes to 200**: the gate was TLS/HTTP-2 fingerprinting. The library matrix below is now relevant.
- **403 unchanged (often identical body, identical error code)**: the gate is upstream of TLS. Likely IP rep, ASN, or per-customer ML scoring of the egress. Stop. No library swap fixes this; you need a different egress or a different access pattern.
This triage takes thirty seconds and removes about half the "which TLS library should I use" questions before they happen.
## When each library wins
| Use case | Pick | Why |
|---|---|---|
| Python scraper, need to match Chrome/Firefox/Safari JA3+JA4+HTTP/2 today | **lexiforest/curl_cffi** | One-liner `impersonate="chrome131"`; presets re-baked each Chrome stable; the simplest baseline for Python experiments. |
| You need the C binary (shell, Tauri sidecar, polyglot env) | **lexiforest/curl-impersonate** | Active fork of the dead lwthiker tree; ships prebuilt `curl_chrome131` etc. New Chrome targets land here. |
| Rust service, native-typed TLS + HTTP/2 without shelling out | **0x676e67/wreq** | Pure Rust on a BoringSSL fork, `reqwest`-shaped builder. HTTP/2 SETTINGS / WINDOW_UPDATE / pseudo-header order are first-class config, not patched overrides on a C stack. `wreq-python` for the PyO3 binding. |
| Go service wants Firefox/Chrome JA3 in an existing `http.Transport` chain | **refraction-networking/utls** | Drop-in `tls.Client` replacement; integrates with `golang.org/x/net/http2`. Caveat: utls only ships TLS — HTTP/2 fingerprint fidelity is *your* problem. It's a Go `crypto/tls` fork that reproduces specific browser ClientHellos (Chrome, Firefox, Edge, Safari) in pure Go, not a link against real BoringSSL/NSS. |
| You're stuck on Node and CycleTLS works for the target | **CycleTLS** | Path of least resistance from Node. Verify HTTP/2 fidelity against the release you pulled; presets drift. Otherwise prefer subprocessing `curl_cffi`. |
| You're *measuring* a fingerprint, not producing one | **FoxIO-LLC/ja4 + tls.peet.ws** | The JA4 spec + reference impls; pair with the `verify_cffi_ja4` probe above. |
| You inherited salesforce/ja3 | **Migrate** | Repo archived 2025-05. JA4 is the modern superset; the JA3 hash is unstable post Chrome 110 anyway. |
Two structural points the table doesn't show:
`curl_cffi` is not a parallel implementation of curl-impersonate — it's CFFI bindings to the *same* C library. Everything `curl-impersonate` patches at TLS / HTTP-2 / header layers, `curl_cffi` inherits by construction. Choose between them on language ergonomics, not capability.
`wreq` matters because it's the first non-libcurl impersonation client where HTTP/2 fingerprint config is a builder field, not a downstream patch on someone else's HTTP/2 stack. For high-concurrency Rust services that's the difference between one task per request on tokio and libcurl's multi-handle.
## What none of them solve
A perfect TLS + HTTP/2 + header replay only gets you past layers 1–3 of the [detection hierarchy](/article/bot-detection-2026/#the-detection-hierarchy). Layer 0 — the TCP/IP fingerprint, including JA4T — is owned by the kernel of whatever host (or proxy) your packets leave from. No impersonation library rewrites TCP options, window scale, or initial TTL. If your proxy is Linux and your User-Agent claims Windows, that's a contradiction the library cannot fix.
Layers 5–7 — cross-header consistency, IP reputation, behavioural analysis — sit outside library scope too. The library will happily send `sec-ch-ua-platform: "Windows"` alongside a `Mac OS X` User-Agent; making them agree is your job. It does not pick the egress ASN, so datacenter classification is your problem. It cannot pace requests, vary access patterns, or fake mouse movement. CAPTCHAs and Turnstile require a browser to render and are irrelevant to HTTP-only clients in either direction (you can't pass them, but they also can't fire). Per-customer ML models (Cloudflare 2025) train on each site's specific baseline, so "looking like Chrome globally" is no longer enough — you have to look like the *kind* of Chrome that visits *that site*. See [bot-detection-2026](/article/bot-detection-2026/) for what owns the rest of the stack.
## Sources
- [lexiforest/curl-impersonate](https://github.com/lexiforest/curl-impersonate) — actively maintained fork (v1.5.6, 2026-05-02).
- [lexiforest/curl_cffi](https://github.com/lexiforest/curl_cffi) — Python binding over libcurl-impersonate.
- [lwthiker/curl-impersonate](https://github.com/lwthiker/curl-impersonate) — original project, unmaintained since 2024.
- [0x676e67/wreq](https://github.com/0x676e67/wreq) — Rust impersonation client on a vendored BoringSSL fork.
- [0x676e67/wreq-python](https://github.com/0x676e67/wreq-python) — PyO3 binding; the project formerly known as `rnet`.
- [refraction-networking/utls](https://github.com/refraction-networking/utls) — Go TLS library with ClientHello control (v1.8.2, 2026-01-13).
- [Danny-Dasilva/CycleTLS](https://github.com/Danny-Dasilva/CycleTLS) — Node + Go impersonation bridge.
- [FoxIO-LLC/ja4](https://github.com/FoxIO-LLC/ja4) — JA4 specification and reference implementation.
- [salesforce/ja3](https://github.com/salesforce/ja3) — archived 2025-05; historical reference only.
- [RFC 8446 §4.1.2](https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2) — TLS 1.3 ClientHello structure.
- [RFC 9113](https://datatracker.ietf.org/doc/html/rfc9113) — HTTP/2, including SETTINGS (§6.5) and pseudo-header rules (§8.3).