Overview

CycleTLS is a Python HTTP client that impersonates real browsers at the TLS layer. It wraps a Go shared library via FFI so that every TLS handshake – cipher suites, extensions, elliptic curves, and HTTP/2 settings – exactly matches a target browser profile.

Why CycleTLS?

Many websites fingerprint incoming TLS connections to distinguish bots from real users. Standard Python HTTP libraries (requests, httpx, urllib3) produce TLS handshakes that look nothing like a browser, making them trivial to block.

CycleTLS solves this by delegating TLS to a Go binary that supports:

  • JA3 fingerprinting – control the exact cipher suites, extensions, and curves advertised during the TLS Client Hello.

  • JA4R fingerprinting – fine-grained control over signature algorithms and extension ordering.

  • HTTP/2 fingerprinting – match browser-specific SETTINGS frames, window sizes, and pseudo-header ordering.

  • HTTP/3 and QUIC – modern protocol support with QUIC fingerprinting.

Architecture

Python (your code)
    |
    v
cycletls Python package
    |  Pydantic models, session management,
    |  cookie jar, async support
    v
FFI boundary (ctypes / ormsgpack)
    |
    v
Go shared library (.so / .dylib / .dll)
    |  utls (uTLS) for TLS fingerprinting
    |  net/http for HTTP/1.1, HTTP/2, HTTP/3
    v
Target server

The Go library is bundled inside the Python wheel, so pip install cycletls is all you need – no Go toolchain required at runtime.

Comparison with Alternatives

Feature

CycleTLS

requests

httpx

curl-cffi

JA3 fingerprinting

Yes

No

No

Yes

JA4R fingerprinting

Yes

No

No

No

HTTP/2 fingerprinting

Yes

No

Partial

Yes

HTTP/3 / QUIC

Yes

No

No

Partial

Browser profiles

Built-in

No

No

Built-in

Async support

Yes

No

Yes

Yes

Session / cookies

Yes

Yes

Yes

Yes

WebSocket

Yes

No

No

No

SSE (Server-Sent Events)

Yes

No

Yes

No

Binary uploads

Yes

Yes

Yes

Yes

Connection pooling

Yes

Yes

Yes

Yes

Pure Python install

Yes (bundled Go binary)

Yes

Yes

No (needs libcurl)

Three Usage Patterns

CycleTLS supports three patterns to fit different needs:

Pattern 1: Simple API (zero boilerplate)

import cycletls

response = cycletls.get("https://example.com")
print(response.status_code)

Module-level functions (get, post, put, patch, delete, head, options) manage a global session automatically. Configure defaults once with cycletls.set_default(...) and every subsequent call inherits them.

Pattern 2: Manual client

from cycletls import CycleTLS

with CycleTLS() as client:
    response = client.get("https://example.com")

Gives you an explicit client lifetime via context manager.

Pattern 3: Session (persistent cookies and headers)

from cycletls import Session

with Session(base_url="https://api.example.com") as s:
    s.headers["Authorization"] = "Bearer token"
    s.post("/login", json={"user": "admin", "pass": "secret"})
    profile = s.get("/profile")  # cookies and headers persist

Session extends CycleTLS with a cookie jar, persistent headers, optional base_url, and auth=(user, password) for HTTP Basic authentication.

Key Concepts

TLS Fingerprints

A TLS fingerprint is a compact description of the parameters a client advertises during the TLS handshake. CycleTLS lets you supply these as strings (ja3=, ja4r=, http2_fingerprint=) or as a named TLSFingerprint profile object (fingerprint=CHROME_120).

Browser Fingerprint Profiles

Built-in profiles bundle JA3, HTTP/2 fingerprint, and User-Agent into a single reusable object:

from cycletls import CHROME_120, FIREFOX_121, SAFARI_17

response = cycletls.get("https://example.com", fingerprint=CHROME_120)

Available built-in profiles: CHROME_120, CHROME_121, FIREFOX_121, SAFARI_17, EDGE_120, CHROME_ANDROID, SAFARI_IOS.

You can also create custom profiles, load them from JSON files, or register them in a directory. See the Advanced Usage guide for details.

Async Support

Every sync method has an async counterpart prefixed with a:

import asyncio
import cycletls

async def main():
    response = await cycletls.aget("https://example.com")
    print(response.status_code)

asyncio.run(main())

Async requests use a callback-based FFI path for lower latency and support asyncio.gather for concurrent execution.