Agent context packet

Structured metadata, source alternates, graph links, headings, series position, and diagram inventory for crawlers and agent readers.

Table of contents

  1. Quick Reference: capability matrix
  2. The cheap triage before reaching for a library
  3. When each library wins
  4. What none of them solve
  5. Sources

Entry facts

Kind
article
Maturity
budding
Confidence
medium
Origin
ai-drafted (AI-drafted, human-reviewed)
Author
Agent
Directed by
krow
Published
Modified
Words
1,447 (7 min read)
Tags
tls, ja4, fingerprinting, scraping, python, rust
Prerequisites
Full corpus
/llms-full.txt
Readable corpus
/source/full-corpus/

Graph links

Prerequisites bot-detection-2026

Related bot-detection-2026ja4-vs-ja3ja4-fingerprint-t13d1516h2

Tagstls, ja4, fingerprinting, scraping, python, rust

TLS Impersonation Libraries: curl_cffi, utls, wreq

TLS impersonation libraries compared — curl_cffi, wreq, utls, CycleTLS, curl-impersonate: which layers each replays, where they break, which to pick.

/ directed by / / 7 min read
On this page

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.

LibraryLangJA3JA4HTTP/2 fpHeader orderReal BoringSSL/NSSMaintLast release
lexiforest/curl_cffiPython (cffi → libcurl-impersonate)activev0.15.1b1 (2026-04-23)
lexiforest/curl-impersonateC / curl fork (active)activev1.5.6 (2026-05-02); v2.0.0a1 prerelease 2026-05-15 in progress
lwthiker/curl-impersonateC / curl fork (original)unmaintained since 2024v0.6.1 (2024-03-02)
0x676e67/wreqRust✅ (BoringSSL fork)activev6.0.0-rc.28 (2026-02-11)
0x676e67/wreq-python (formerly rnet)Python (PyO3 → wreq)activetracks wreq
refraction-networking/utlsGo (crypto/tls drop-in)partial (TLS only)manual (your x/net/http2)manualGo crypto/tls fork that mimics browser ClientHellos (Chrome, Firefox, Edge, Safari)activev1.8.2 (2026-01-13)
Danny-Dasilva/CycleTLSNode + Go subprocesspartial (preset-dependent)weakpartialuses utlspartial maintcycletls/v2.0.3
FoxIO-LLC/ja4spec + Rust/Go/Python toolingn/a✅ (defines JA4)✅ (JA4H/S/T family)n/an/aactivev0.18.8 (2025-11-19)
salesforce/ja3spec + Pythonn/aarchived 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:

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; the JA3-vs-JA4 background is in JA3 vs JA4. 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 casePickWhy
Python scraper, need to match Chrome/Firefox/Safari JA3+JA4+HTTP/2 todaylexiforest/curl_cffiOne-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-impersonateActive 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 out0x676e67/wreqPure 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 chainrefraction-networking/utlsDrop-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 targetCycleTLSPath 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 oneFoxIO-LLC/ja4 + tls.peet.wsThe JA4 spec + reference impls; pair with the verify_cffi_ja4 probe above.
You inherited salesforce/ja3MigrateRepo 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. 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 for what owns the rest of the stack.

Sources

Diagram

Drag to pan · scroll or pinch to zoom · Esc to close