Skip to main content

Advanced TLS fingerprinting library with JA3, JA4, HTTP/2, HTTP/3, WebSocket, and SSE support - bypass anti-bot detection by impersonating real browsers

Project description

CycleTLS Python

CI PyPI version Downloads Python versions license

Python HTTP client that impersonates real browsers - bypass anti-bot detection with TLS/JA3/JA4/HTTP2 fingerprinting

Advanced TLS fingerprinting library with JA3, JA4, HTTP/2, HTTP/3, WebSocket, and SSE support.

Unlike requests or httpx, CycleTLS can make your requests indistinguishable from real browser traffic.

If you have an API change or feature request feel free to open an Issue

๐Ÿš€ Features

  • Async/Await Support - Full async API with 1.7x performance boost for concurrent requests
  • Advanced TLS Fingerprinting - JA3, JA4R, and HTTP/2 fingerprinting support
  • HTTP/3 and QUIC - Modern protocol support with QUIC fingerprinting
  • Pythonic API - Familiar requests-like interface with context managers
  • Connection Pooling - Built-in connection reuse for high performance
  • Comprehensive Proxy Support - HTTP, HTTPS, SOCKS4, SOCKS5, SOCKS5h
  • WebSocket & SSE - Full bidirectional WebSocket and Server-Sent Events support
  • Binary Data Handling - Seamless upload and download of binary content
  • Type-Safe - Pydantic models with full type hints
  • Session Management - Persistent cookies and headers across requests
  • ๐Ÿ†• Browser Fingerprint Profiles - Built-in Chrome, Firefox, Safari, Edge profiles with plugin support
  • ๐Ÿ†• Zero-Copy FFI - 3x faster sync requests with optimized Python-Go communication

Table of Contents

Dependencies

python ^3.8
golang ^1.21x (for building from source)

Installation

With uv (Recommended):

uv add cycletls

With pip:

pip install cycletls

Quick Start

Simple API (Zero Boilerplate) - NEW! ๐ŸŽ‰

import cycletls

# That's it! Auto-setup, auto-cleanup
response = cycletls.get('https://httpbin.org/get')
print(response.status_code)  # 200
print(response.json())

Configure Once, Use Everywhere

import cycletls

# Set defaults for all requests
cycletls.set_default(
    proxy='socks5://127.0.0.1:9050',
    timeout=10,
    ja3='771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0',
    user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
)

# All future requests use these defaults
response1 = cycletls.get('https://httpbin.org/get')
response2 = cycletls.post('https://httpbin.org/post', json_data={'key': 'value'})

# Per-request overrides
response3 = cycletls.get('https://httpbin.org/get', timeout=5)  # Override timeout

Three Usage Patterns

CycleTLS supports three patterns to fit your needs:

# Pattern 1: Simple API (NEW) - Zero boilerplate, like requests
import cycletls
response = cycletls.get('https://example.com')

# Pattern 2: Manual Client - Full control
from cycletls import CycleTLS
with CycleTLS() as client:
    response = client.get('https://example.com')

# Pattern 3: Session - Persistent cookies/headers
from cycletls import Session
with Session() as session:
    session.headers['Authorization'] = 'Bearer token'
    response1 = session.post('/login', json_data={...})
    response2 = session.get('/profile')  # Cookies preserved

With TLS Fingerprinting

import cycletls

# Chrome 83 fingerprint - Simple API
response = cycletls.get(
    'https://ja3er.com/json',
    ja3='771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0',
    user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
)
print(f"JA3 Hash: {response.json()['ja3_hash']}")

Usage

Basic Requests

Simple API (Module-Level Functions)

import cycletls

# All HTTP methods available as module-level functions
response = cycletls.get('https://httpbin.org/get')
response = cycletls.post('https://httpbin.org/post', json_data={'key': 'value'})
response = cycletls.put('https://httpbin.org/put', json_data={'status': 'updated'})
response = cycletls.patch('https://httpbin.org/patch', json_data={'field': 'value'})
response = cycletls.delete('https://httpbin.org/delete')
response = cycletls.head('https://httpbin.org/get')
response = cycletls.options('https://httpbin.org/get')

# POST with form data
response = cycletls.post(
    'https://httpbin.org/post',
    data={'username': 'john', 'password': 'secret'}
)

Manual Client (Context Manager)

from cycletls import CycleTLS

with CycleTLS() as client:
    # GET request
    response = client.get('https://httpbin.org/get')

    # POST with JSON data
    response = client.post(
        'https://httpbin.org/post',
        json_data={'key': 'value'}
    )

    # POST with form data
    response = client.post(
        'https://httpbin.org/post',
        data={'username': 'john', 'password': 'secret'}
    )

    # Other methods
    response = client.put('https://httpbin.org/put', json_data={'status': 'updated'})
    response = client.patch('https://httpbin.org/patch', json_data={'field': 'value'})
    response = client.delete('https://httpbin.org/delete')
    response = client.head('https://httpbin.org/get')
    response = client.options('https://httpbin.org/get')

Response Properties

from cycletls import CycleTLS

with CycleTLS() as client:
    response = client.get('https://httpbin.org/get')

    # Status information
    print(response.status_code)  # 200
    print(response.ok)  # True (200-399)
    print(response.reason)  # "OK"

    # Content access
    print(response.text)  # Response as string
    print(response.content)  # Response as bytes
    print(response.json())  # Parse JSON

    # Headers (case-insensitive)
    print(response.headers['Content-Type'])
    print(response.headers.get('content-type'))  # Same as above

    # Cookies
    print(response.cookies['session_id'])

    # URL after redirects
    print(response.url)

    # Encoding detection
    print(response.encoding)  # 'utf-8'

    # Error checking
    if response.is_error:
        print(f"Error: {response.status_code}")

    # Raise exception on error
    response.raise_for_status()  # Raises HTTPError if status >= 400

Async API (async/await Support)

CycleTLS now provides full async/await support for concurrent request handling, offering 1.7x performance improvement for I/O-bound workloads.

Simple Async Requests

import asyncio
import cycletls

async def main():
    # Module-level async functions
    response = await cycletls.aget('https://httpbin.org/get')
    print(response.status_code)  # 200

    # POST with JSON
    response = await cycletls.apost(
        'https://httpbin.org/post',
        json_data={'key': 'value'}
    )

    # Other async methods
    await cycletls.aput('https://httpbin.org/put', json_data={...})
    await cycletls.apatch('https://httpbin.org/patch', json_data={...})
    await cycletls.adelete('https://httpbin.org/delete')
    await cycletls.ahead('https://httpbin.org/get')
    await cycletls.aoptions('https://httpbin.org/get')

asyncio.run(main())

Async Context Manager

import asyncio
from cycletls import AsyncCycleTLS

async def main():
    async with AsyncCycleTLS() as client:
        # Reuse client for multiple requests
        response1 = await client.get('https://httpbin.org/get')
        response2 = await client.post('https://httpbin.org/post', json_data={})
        response3 = await client.put('https://httpbin.org/put', json_data={})

asyncio.run(main())

Concurrent Requests (The Power of Async!)

import asyncio
import cycletls

async def main():
    # Make 10 requests concurrently - all execute in parallel!
    responses = await asyncio.gather(*[
        cycletls.aget(f'https://httpbin.org/get?id={i}')
        for i in range(10)
    ])

    print(f"Completed {len(responses)} requests")
    print(f"All successful: {all(r.status_code == 200 for r in responses)}")

asyncio.run(main())

Performance Comparison

import asyncio
import time
import cycletls

async def benchmark():
    urls = [f'https://httpbin.org/delay/1?id={i}' for i in range(5)]

    # Sequential (slow)
    start = time.time()
    for url in urls:
        await cycletls.aget(url)
    sequential_time = time.time() - start

    # Concurrent (fast!)
    start = time.time()
    await asyncio.gather(*[cycletls.aget(url) for url in urls])
    concurrent_time = time.time() - start

    print(f"Sequential: {sequential_time:.2f}s")
    print(f"Concurrent: {concurrent_time:.2f}s")
    print(f"Speedup: {sequential_time / concurrent_time:.2f}x")
    # Output: Speedup: ~5.0x

asyncio.run(benchmark())

Async with TLS Fingerprinting

import asyncio
import cycletls

async def main():
    # Async works with all CycleTLS features!
    chrome_ja3 = "771,4865-4866-4867-49195-49199-49196-49200..."

    # Single async request with fingerprint
    response = await cycletls.aget(
        'https://ja3er.com/json',
        ja3=chrome_ja3,
        user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    )

    # Concurrent requests with fingerprints
    tasks = [
        cycletls.aget('https://ja3er.com/json', ja3=chrome_ja3),
        cycletls.aget('https://ja3er.com/json', ja3=firefox_ja3),
        cycletls.aget('https://ja3er.com/json', ja3=safari_ja3),
    ]
    responses = await asyncio.gather(*tasks)

asyncio.run(main())

Async Configuration

Async requests support the same configuration options as sync requests:

async with AsyncCycleTLS() as client:
    response = await client.get(
        'https://httpbin.org/delay/5',
        timeout=10.0,           # Max time to wait for response (seconds)
        poll_interval=0.01,     # Polling interval (seconds), 0.0 = adaptive
        proxy='socks5://127.0.0.1:9050',
        ja3='...',
        user_agent='...',
        headers={'X-Custom': 'Header'},
        cookies={'session': 'abc123'}
    )

Polling Behavior:

  • poll_interval=0.0 (default): Adaptive polling (tight loop โ†’ 100ฮผs โ†’ 1ms)
  • poll_interval=0.01: Fixed 10ms polling interval
  • timeout=30.0 (default): Request timeout in seconds

Rate Limiting with Semaphore

import asyncio
import cycletls

async def main():
    # Limit to 5 concurrent requests at a time
    semaphore = asyncio.Semaphore(5)

    async def limited_request(url):
        async with semaphore:
            return await cycletls.aget(url)

    # Launch 100 requests, but only 5 run concurrently
    responses = await asyncio.gather(*[
        limited_request(f'https://httpbin.org/get?id={i}')
        for i in range(100)
    ])

asyncio.run(main())

Error Handling

import asyncio
import cycletls
from cycletls.exceptions import HTTPError

async def main():
    # Handle HTTP errors
    response = await cycletls.aget('https://httpbin.org/status/404')
    if response.is_error:
        print(f"Error: {response.status_code}")

    # Raise exception on error
    try:
        response.raise_for_status()
    except HTTPError as e:
        print(f"HTTP Error: {e}")

    # Handle timeouts
    try:
        response = await cycletls.aget(
            'https://httpbin.org/delay/10',
            timeout=2.0
        )
    except asyncio.TimeoutError:
        print("Request timed out")

    # Concurrent requests with error handling
    results = await asyncio.gather(
        cycletls.aget('https://httpbin.org/status/200'),
        cycletls.aget('https://httpbin.org/status/404'),
        return_exceptions=True  # Don't stop on first error
    )

    for result in results:
        if isinstance(result, Exception):
            print(f"Error: {result}")
        else:
            print(f"Success: {result.status_code}")

asyncio.run(main())

Available Async Functions:

  • cycletls.aget() - Async GET request
  • cycletls.apost() - Async POST request
  • cycletls.aput() - Async PUT request
  • cycletls.apatch() - Async PATCH request
  • cycletls.adelete() - Async DELETE request
  • cycletls.ahead() - Async HEAD request
  • cycletls.aoptions() - Async OPTIONS request
  • cycletls.async_request() - Generic async request

Performance Benefits:

  • 1.7x faster for concurrent I/O-bound workloads
  • Efficient CPU usage with adaptive polling
  • Non-blocking: thousands of concurrent requests with minimal overhead
  • Perfect for web scraping, API aggregation, and bulk data fetching

Configuration

Module-Level Defaults

Set default values once and have them apply to all requests:

import cycletls

# Configure defaults once
cycletls.set_default(
    proxy='socks5://127.0.0.1:9050',
    timeout=10,
    ja3='771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0',
    user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    enable_connection_reuse=True,
    insecure_skip_verify=False
)

# All subsequent requests use these defaults
response1 = cycletls.get('https://api.example.com/endpoint1')
response2 = cycletls.get('https://api.example.com/endpoint2')

# Override defaults per-request
response3 = cycletls.get('https://api.example.com/endpoint3', timeout=30)

Available Configuration Options:

Option Type Description
ja3 str JA3 TLS fingerprint string
ja4r str JA4 raw format fingerprint
http2_fingerprint str HTTP/2 fingerprint
quic_fingerprint str QUIC fingerprint
disable_grease bool Disable GREASE for exact JA4 matching
user_agent str User-Agent header
proxy str Proxy URL (http/https/socks4/socks5)
timeout int Request timeout in seconds
enable_connection_reuse bool Enable connection pooling
insecure_skip_verify bool Skip TLS certificate verification
server_name str Custom SNI (Server Name Indication)
force_http1 bool Force HTTP/1.1 protocol
force_http3 bool Force HTTP/3 protocol
protocol str Protocol selection (http1/http2/http3)
disable_redirect bool Disable automatic redirects
header_order list Custom header ordering
order_headers_as_provided bool Use provided header order

Read Configuration

import cycletls

# Set a default
cycletls.set_default(timeout=10)

# Read configuration value
timeout = cycletls.get_default('timeout')
print(f"Default timeout: {timeout}")  # 10

# Read via module attribute
timeout = cycletls.default_timeout
print(f"Default timeout: {timeout}")  # 10

Reset Configuration

import cycletls

# Configure defaults
cycletls.set_default(proxy='socks5://127.0.0.1:9050', timeout=10)

# Reset all defaults
cycletls.reset_defaults()

# All defaults are now cleared

Manual Session Cleanup

The global session is automatically cleaned up on program exit, but you can manually close it:

import cycletls

response = cycletls.get('https://example.com')

# Manually close the global session (useful in notebooks)
cycletls.close_global_session()

# Next call creates a new session
response = cycletls.get('https://example.com')

TLS Fingerprinting

JA3 Fingerprinting

JA3 fingerprinting allows you to mimic specific browser TLS implementations:

Simple API:

import cycletls

# Browser fingerprints
BROWSER_FINGERPRINTS = {
    'chrome_83': {
        'ja3': '771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0',
        'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'
    },
    'firefox_87': {
        'ja3': '771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-51-43-13-45-28-21,29-23-24-25-256-257,0',
        'user_agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0'
    },
    'safari_15': {
        'ja3': '771,4865-4867-4866-49196-49195-52393-49200-49199-52392-49162-49161-49172-49171-157-156-53-47-49160-49170-10,0-23-65281-10-11-35-16-5-13-45-28-21,29-23-24-25,0',
        'user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15'
    }
}

# Use Chrome 83 fingerprint
chrome_fp = BROWSER_FINGERPRINTS['chrome_83']
response = cycletls.get(
    'https://ja3er.com/json',
    ja3=chrome_fp['ja3'],
    user_agent=chrome_fp['user_agent']
)

data = response.json()
print(f"JA3 Hash: {data['ja3_hash']}")
print(f"User Agent: {data['User-Agent']}")

Manual Client:

from cycletls import CycleTLS

with CycleTLS() as client:
    chrome_fp = BROWSER_FINGERPRINTS['chrome_83']
    response = client.get(
        'https://ja3er.com/json',
        ja3=chrome_fp['ja3'],
        user_agent=chrome_fp['user_agent']
    )

    data = response.json()
    print(f"JA3 Hash: {data['ja3_hash']}")
    print(f"User Agent: {data['User-Agent']}")

JA4R Fingerprinting (Advanced)

Important: Use ja4r (raw format) to configure TLS fingerprints. JA4 hashes are for observation only.

JA4R provides explicit control over cipher suites, extensions, and signature algorithms:

from cycletls import CycleTLS

with CycleTLS() as client:
    # Chrome 138 JA4R fingerprint
    response = client.get(
        'https://tls.peet.ws/api/all',
        ja4r='t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0000,0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601',
        disable_grease=False,
        user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'
    )

    data = response.json()
    print(f"JA4: {data['tls']['ja4']}")
    print(f"JA4_r: {data['tls']['ja4_r']}")
    print(f"TLS Version: {data['tls']['tls_version_negotiated']}")

JA4R Format Breakdown:

t13d1516h2_<ciphers>_<extensions>_<signature_algorithms>
โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚   โ”‚        โ”‚            โ”‚
โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚   โ”‚        โ”‚            โ””โ”€ Signature algorithms (0x0403, 0x0804, ...)
โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚   โ”‚        โ””โ”€ Extensions (0x0000=SNI, 0x000a=supported_groups, ...)
โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚   โ””โ”€ Cipher suites (0x002f=AES128, 0x1301=TLS_AES_128, ...)
โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ””โ”€ HTTP version (h2=HTTP/2)
โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ””โ”€ Max fragment length (1516 bytes)
โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ””โ”€ ALPN first value length
โ”‚โ”‚โ”‚โ”‚โ”‚โ””โ”€ Extension count (13 decimal)
โ”‚โ”‚โ”‚โ”‚โ””โ”€ TLS version (1.3)
โ”‚โ”‚โ”‚โ””โ”€ QUIC support
โ”‚โ”‚โ””โ”€ TLS version
โ”‚โ””โ”€ Transport (t=TCP, q=QUIC)
โ””โ”€ Type (t=standard)

HTTP/2 Fingerprinting

Mimic specific browser HTTP/2 implementations:

from cycletls import CycleTLS

with CycleTLS() as client:
    # Firefox HTTP/2 fingerprint
    response = client.get(
        'https://tls.peet.ws/api/all',
        http2_fingerprint='1:65536;2:0;4:131072;5:16384|12517377|0|m,p,a,s',
        ja3='771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-51-43-13-45-28-21,29-23-24-25-256-257,0',
        user_agent='Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0'
    )

    data = response.json()
    print(f"HTTP/2 Fingerprint: {data['http2']['akamai_fingerprint']}")
    print(f"Settings: {data['http2']['sent_frames'][0]['settings']}")

Common Browser HTTP/2 Fingerprints:

Browser HTTP/2 Fingerprint Description
Firefox 1:65536;2:0;4:131072;5:16384|12517377|0|m,p,a,s Smaller window, MPAS priority
Chrome 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p Larger window, MASP priority

HTTP/2 Fingerprint Format:

<settings>|<window_update>|<priority>|<pseudo_header_order>
โ”‚          โ”‚               โ”‚          โ”‚
โ”‚          โ”‚               โ”‚          โ””โ”€ Pseudo-header order (m=method, p=path, a=authority, s=scheme)
โ”‚          โ”‚               โ””โ”€ Priority (0=no priority frame)
โ”‚          โ””โ”€ Window update value
โ””โ”€ Settings (1=header_table_size, 2=enable_push, 4=max_concurrent_streams, ...)

Browser Fingerprint Profiles (NEW!)

Use built-in browser profiles instead of manually configuring JA3/JA4/HTTP2 strings:

import cycletls
from cycletls import CHROME_120, FIREFOX_121, SAFARI_17, FingerprintRegistry

# Use built-in browser profile
response = cycletls.get(
    'https://ja3er.com/json',
    fingerprint=CHROME_120  # Pre-configured Chrome 120 fingerprint
)

# Available built-in profiles
profiles = [
    CHROME_120,      # Chrome 120 on Windows
    CHROME_121,      # Chrome 121 on Windows
    FIREFOX_121,     # Firefox 121 on Linux
    SAFARI_17,       # Safari 17 on macOS
    EDGE_120,        # Edge 120 on Windows
    CHROME_ANDROID,  # Chrome on Android
    SAFARI_IOS,      # Safari on iOS
]

# List all registered profiles
registry = FingerprintRegistry()
for name in registry.all():
    print(f"Available: {name}")

Custom Fingerprint Profiles:

from cycletls import TLSFingerprint, FingerprintRegistry

# Create custom fingerprint
my_profile = TLSFingerprint(
    name='custom_browser',
    ja3='771,4865-4866-4867-49195...',
    user_agent='Mozilla/5.0...',
    http2_fingerprint='1:65536;2:0;4:131072...',
)

# Register for reuse
registry = FingerprintRegistry()
registry.register(my_profile)

# Use in requests
response = cycletls.get('https://example.com', fingerprint=my_profile)

Load Fingerprints from Files:

from cycletls import load_fingerprints_from_dir, load_fingerprint_from_file

# Load single profile from JSON/YAML
profile = load_fingerprint_from_file('profiles/chrome_125.json')

# Load all profiles from a directory
load_fingerprints_from_dir('profiles/')  # Auto-registers all found profiles

# Use environment variable for plugin directory
# Set CYCLETLS_FINGERPRINT_DIR=/path/to/profiles
from cycletls import load_fingerprints_from_env
load_fingerprints_from_env()

HTTP/3 Support

from cycletls import CycleTLS

with CycleTLS() as client:
    # Force HTTP/3
    response = client.get(
        'https://cloudflare-quic.com/',
        force_http3=True,
        user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
        insecure_skip_verify=True
    )

    print(f"Response over HTTP/3: {response.status_code}")

Proxy Configuration

CycleTLS supports multiple proxy protocols:

Simple API (with defaults):

import cycletls

# Set proxy as default
cycletls.set_default(proxy='socks5://127.0.0.1:9050')

# All requests use the proxy
response = cycletls.get('https://httpbin.org/ip')
print(response.json())

Simple API (per-request):

import cycletls

# HTTP Proxy with authentication
response = cycletls.get(
    'https://httpbin.org/ip',
    proxy='http://username:password@proxy.example.com:8080'
)

# SOCKS5 Proxy (Tor example)
response = cycletls.get(
    'https://httpbin.org/ip',
    proxy='socks5://127.0.0.1:9050'
)

print(response.json())

Supported Proxy Protocols:

  • http:// - HTTP proxy
  • https:// - HTTPS proxy
  • socks4:// - SOCKS4 proxy
  • socks5:// - SOCKS5 proxy
  • socks5h:// - SOCKS5 with hostname resolution through proxy

Manual Client:

from cycletls import CycleTLS

with CycleTLS() as client:
    # HTTP Proxy with authentication
    response = client.get(
        'https://httpbin.org/ip',
        proxy='http://username:password@proxy.example.com:8080'
    )

    # SOCKS5 Proxy
    response = client.get(
        'https://httpbin.org/ip',
        proxy='socks5://127.0.0.1:9050'
    )

    print(response.json())

Cookie Handling

Simple Cookie Dict

Simple API:

import cycletls

# Send cookies as dict
response = cycletls.get(
    'https://httpbin.org/cookies',
    cookies={'session_id': 'abc123', 'user_token': 'xyz789'}
)

print(response.json())

Manual Client:

from cycletls import CycleTLS

with CycleTLS() as client:
    response = client.get(
        'https://httpbin.org/cookies',
        cookies={'session_id': 'abc123', 'user_token': 'xyz789'}
    )

    print(response.json())

Advanced Cookie Objects

from cycletls import CycleTLS, Cookie

with CycleTLS() as client:
    # Create Cookie objects with full attributes
    cookies = [
        Cookie(
            name='session_id',
            value='abc123',
            domain='httpbin.org',
            path='/',
            secure=True,
            http_only=True,
            same_site='Lax'
        ),
        Cookie(
            name='preferences',
            value='dark_mode',
            max_age=3600
        )
    ]

    response = client.get('https://httpbin.org/cookies', cookies=cookies)
    print(response.json())

Response Cookies

from cycletls import CycleTLS

with CycleTLS() as client:
    # Server sets cookies
    response = client.get('https://httpbin.org/cookies/set?name=value')

    # Access response cookies
    print(response.cookies['name'])  # 'value'

    # CookieJar interface
    for name in response.cookies:
        print(f"{name}: {response.cookies[name]}")

Binary Data

Download Binary Data

Simple API:

import cycletls

# Download image
response = cycletls.get('https://httpbin.org/image/jpeg')

# Access binary content
image_data = response.content  # bytes

# Save to file
with open('image.jpg', 'wb') as f:
    f.write(image_data)

print(f"Downloaded {len(image_data)} bytes")

Manual Client:

from cycletls import CycleTLS

with CycleTLS() as client:
    response = client.get('https://httpbin.org/image/jpeg')

    with open('image.jpg', 'wb') as f:
        f.write(response.content)

    print(f"Downloaded {len(response.content)} bytes")

Upload Binary Data

Simple API:

import cycletls

# Read binary data
with open('image.jpg', 'rb') as f:
    binary_data = f.read()

# Upload binary data
response = cycletls.post(
    'https://httpbin.org/post',
    data=binary_data,
    headers={'Content-Type': 'image/jpeg'}
)

print(response.json())

Manual Client:

from cycletls import CycleTLS

with CycleTLS() as client:
    with open('image.jpg', 'rb') as f:
        binary_data = f.read()

    # Upload using body_bytes
    response = client.post(
        'https://httpbin.org/post',
        body_bytes=binary_data,
        headers={'Content-Type': 'application/octet-stream'}
    )

    print(response.status_code)

Supported Media Types

The following content types are automatically handled as binary data:

  • Images: image/jpeg, image/png, image/gif, image/webp, image/svg+xml
  • Videos: video/mp4, video/webm, video/avi, video/quicktime
  • Documents: application/pdf

WebSocket Client

Full bidirectional WebSocket support with TLS fingerprinting.

from cycletls import WebSocketConnection

# Basic WebSocket connection
with WebSocketConnection('wss://echo.websocket.org') as ws:
    # Send text message
    ws.send('Hello, WebSocket!')

    # Receive message
    message = ws.receive()
    print(f"Received: {message.data}")
    print(f"Message type: {message.type}")  # MessageType.TEXT or MessageType.BINARY

# WebSocket with TLS fingerprinting
with WebSocketConnection(
    'wss://example.com/socket',
    ja3='771,4865-4867-4866-49195-49199...',
    user_agent='Mozilla/5.0...',
    headers={'Authorization': 'Bearer token'},
    proxy='socks5://127.0.0.1:9050'
) as ws:
    # Send and receive messages
    ws.send('{"action": "subscribe", "channel": "updates"}')

    # Iterate over messages
    for message in ws:
        if message.is_close:
            break
        print(f"Event: {message.data}")

WebSocket Features:

  • โœ… Full bidirectional messaging (text and binary)
  • โœ… TLS fingerprinting (JA3, JA4R)
  • โœ… Custom headers and proxy support
  • โœ… Context manager for automatic cleanup
  • โœ… Message type detection (TEXT, BINARY, CLOSE, PING, PONG)

Server-Sent Events (SSE)

Full SSE streaming support with TLS fingerprinting.

from cycletls import SSEConnection

# Basic SSE connection
with SSEConnection('https://example.com/events') as sse:
    # Iterate over events
    for event in sse:
        print(f"Event type: {event.event}")  # 'message', 'update', etc.
        print(f"Data: {event.data}")
        print(f"ID: {event.id}")

# SSE with TLS fingerprinting and resume support
with SSEConnection(
    'https://api.example.com/stream',
    ja3='771,4865-4867-4866-49195-49199...',
    user_agent='Mozilla/5.0...',
    headers={'Authorization': 'Bearer token'},
    last_event_id='event-99',  # Resume from last known event
    proxy='socks5://127.0.0.1:9050'
) as sse:
    for event in sse:
        if event.retry:
            print(f"Server requested retry interval: {event.retry}ms")
        process_event(event.data)

SSE Features:

  • โœ… Full event streaming with automatic parsing
  • โœ… TLS fingerprinting (JA3, JA4R)
  • โœ… Last-Event-ID for resumption
  • โœ… Custom retry interval handling
  • โœ… Context manager for automatic cleanup
  • โœ… Event type, ID, and data extraction

Session Management

Use Session for persistent cookies and headers across requests:

from cycletls import Session

# Create session
with Session() as session:
    # Set persistent headers
    session.headers['Authorization'] = 'Bearer token123'
    session.headers['User-Agent'] = 'CustomBot/1.0'

    # Login - cookies are automatically saved
    login_response = session.post(
        'https://httpbin.org/cookies/set?session=abc123',
        json_data={'username': 'admin', 'password': 'secret'}
    )

    # Subsequent requests include cookies and headers automatically
    profile_response = session.get('https://httpbin.org/cookies')
    print(profile_response.json())
    # {'cookies': {'session': 'abc123'}}

    # Add more cookies to session
    session.cookies.set('preferences', 'dark_mode')

    # All future requests will include both cookies
    settings_response = session.get('https://httpbin.org/cookies')
    print(settings_response.json())
    # {'cookies': {'session': 'abc123', 'preferences': 'dark_mode'}}

Session Features:

  • Persistent cookies across requests
  • Persistent headers across requests
  • Automatic cookie updates from responses
  • Request-specific cookies/headers override session values
  • Context manager support for automatic cleanup

Advanced Features

Connection Reuse

Simple API (with defaults):

import cycletls

# Enable connection reuse for all requests
cycletls.set_default(enable_connection_reuse=True)

# First request establishes connection
response1 = cycletls.get('https://httpbin.org/get')

# Second request reuses connection (faster)
response2 = cycletls.get('https://httpbin.org/headers')

Manual Client:

from cycletls import CycleTLS

with CycleTLS() as client:
    # Enable connection reuse for better performance
    response1 = client.get(
        'https://httpbin.org/get',
        enable_connection_reuse=True
    )

    # Second request reuses connection (faster)
    response2 = client.get(
        'https://httpbin.org/headers',
        enable_connection_reuse=True
    )

Custom SNI (Domain Fronting)

from cycletls import CycleTLS

with CycleTLS() as client:
    # Set SNI different from Host header
    response = client.get(
        'https://127.0.0.1:8443',
        server_name='front.example',  # TLS SNI
        headers={'Host': 'real.example'},  # HTTP Host header
        insecure_skip_verify=True
    )

Timeout Configuration

from cycletls import CycleTLS

with CycleTLS() as client:
    try:
        # Set timeout (seconds)
        response = client.get(
            'https://httpbin.org/delay/10',
            timeout=5
        )
    except Exception as e:
        print(f"Request timed out: {e}")

Redirect Handling

from cycletls import CycleTLS

with CycleTLS() as client:
    # Disable automatic redirects
    response = client.get(
        'https://httpbin.org/redirect/3',
        disable_redirect=True
    )

    print(response.status_code)  # 302
    print(response.headers['Location'])

Error Handling

Simple API:

import cycletls
from cycletls import HTTPError, ConnectionError, Timeout

try:
    response = cycletls.get('https://httpbin.org/status/404')
    response.raise_for_status()  # Raises HTTPError
except HTTPError as e:
    print(f"HTTP Error: {e}")
    print(f"Status Code: {e.response.status_code}")
    print(f"Response Body: {e.response.text}")
except ConnectionError as e:
    print(f"Connection Error: {e}")
except Timeout as e:
    print(f"Timeout: {e}")

Manual Client:

from cycletls import CycleTLS, HTTPError, ConnectionError, Timeout

with CycleTLS() as client:
    try:
        response = client.get('https://httpbin.org/status/404')
        response.raise_for_status()  # Raises HTTPError
    except HTTPError as e:
        print(f"HTTP Error: {e}")
        print(f"Status Code: {e.response.status_code}")
        print(f"Response Body: {e.response.text}")
    except ConnectionError as e:
        print(f"Connection Error: {e}")
    except Timeout as e:
        print(f"Timeout: {e}")

Custom Header Ordering

from cycletls import CycleTLS

with CycleTLS() as client:
    response = client.get(
        'https://httpbin.org/headers',
        headers={
            'Accept': 'application/json',
            'User-Agent': 'CustomBot/1.0',
            'Accept-Language': 'en-US'
        },
        header_order=['accept', 'user-agent', 'accept-language'],
        order_headers_as_provided=True
    )

Debugging and Logging

CycleTLS includes comprehensive Python logging support for debugging requests, responses, and FFI operations.

Enable Debug Logging

import logging
import cycletls

# Enable debug logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# Make a request - see detailed logs
response = cycletls.get('https://httpbin.org/get')

Log Output:

DEBUG:cycletls.api:Sending GET request to https://httpbin.org/get
DEBUG:cycletls.api:Request headers: {...}
DEBUG:cycletls._ffi:Calling Go shared library getRequest()
DEBUG:cycletls._ffi:Received response from Go (size: 1234 bytes)
DEBUG:cycletls.api:Received response: 200 OK
DEBUG:cycletls.api:Response headers: {...}

Log Levels

  • DEBUG: Detailed request/response info, headers, FFI calls, response sizes
  • INFO: Library loading events
  • ERROR: Request failures, parsing errors

Selective Logging

import logging

# Only log errors
logging.basicConfig(level=logging.ERROR)

# Or configure specific loggers
logging.getLogger('cycletls.api').setLevel(logging.DEBUG)
logging.getLogger('cycletls._ffi').setLevel(logging.INFO)

Logging with Proxies

import logging
import cycletls

logging.basicConfig(level=logging.DEBUG)

# Proxy usage is logged automatically
response = cycletls.get(
    'https://httpbin.org/ip',
    proxy='socks5://127.0.0.1:9050'
)
# Log: "Using proxy: socks5://127.0.0.1:9050"

API Reference

Module-Level Functions (Simple API)

The Simple API provides convenient module-level functions that use a shared global client.

Synchronous Functions

import cycletls

# Make requests directly
response = cycletls.get(url, **kwargs)
response = cycletls.post(url, data=None, json_data=None, **kwargs)
response = cycletls.put(url, data=None, json_data=None, **kwargs)
response = cycletls.patch(url, data=None, json_data=None, **kwargs)
response = cycletls.delete(url, **kwargs)
response = cycletls.head(url, **kwargs)
response = cycletls.options(url, **kwargs)
response = cycletls.request(method, url, **kwargs)

Async Functions

import asyncio
import cycletls

async def main():
    # Async module-level functions
    response = await cycletls.aget(url, **kwargs)
    response = await cycletls.apost(url, data=None, json_data=None, **kwargs)
    response = await cycletls.aput(url, data=None, json_data=None, **kwargs)
    response = await cycletls.apatch(url, data=None, json_data=None, **kwargs)
    response = await cycletls.adelete(url, **kwargs)
    response = await cycletls.ahead(url, **kwargs)
    response = await cycletls.aoptions(url, **kwargs)
    response = await cycletls.async_request(method, url, **kwargs)

asyncio.run(main())

Async-Specific Parameters:

  • timeout (float): Maximum time to wait for request completion (default: 30.0 seconds)
  • poll_interval (float): Polling interval for checking completion (default: 0.0 = adaptive)
    • 0.0: Adaptive polling (tight loop โ†’ 100ฮผs โ†’ 1ms based on checks)
    • > 0.0: Fixed polling interval in seconds

Configuration Functions:

# Set default values
cycletls.set_default(
    proxy='socks5://127.0.0.1:9050',
    timeout=10,
    ja3='771,4865-4866...',
    # ... any other request parameter
)

# Get a default value
value = cycletls.get_default('timeout')  # Returns 10 or None

# Read via module attribute
timeout = cycletls.default_timeout  # Same as get_default('timeout')

# Reset all defaults
cycletls.reset_defaults()

# Manually close global session (useful in notebooks)
cycletls.close_global_session()

Features:

  • Zero boilerplate - import and use immediately
  • Automatic resource management (no context managers needed)
  • Configurable defaults that persist across requests
  • Thread-safe for concurrent requests
  • Automatic cleanup on program exit
  • Fork-safe (creates new session in child processes)

CycleTLS Class

class CycleTLS:
    def __init__(self):
        """Initialize CycleTLS client."""

    def request(
        self,
        method: str,
        url: str,
        params: Optional[Dict] = None,
        data: Optional[Any] = None,
        json_data: Optional[Dict] = None,
        files: Optional[Dict] = None,
        **kwargs
    ) -> Response:
        """Send an HTTP request."""

    def get(self, url: str, params: Optional[Dict] = None, **kwargs) -> Response:
        """Send a GET request."""

    def post(
        self,
        url: str,
        params: Optional[Dict] = None,
        data: Optional[Any] = None,
        json_data: Optional[Dict] = None,
        **kwargs
    ) -> Response:
        """Send a POST request."""

    def put(self, url: str, data: Optional[Any] = None, json_data: Optional[Dict] = None, **kwargs) -> Response:
        """Send a PUT request."""

    def patch(self, url: str, data: Optional[Any] = None, json_data: Optional[Dict] = None, **kwargs) -> Response:
        """Send a PATCH request."""

    def delete(self, url: str, **kwargs) -> Response:
        """Send a DELETE request."""

    def head(self, url: str, **kwargs) -> Response:
        """Send a HEAD request."""

    def options(self, url: str, **kwargs) -> Response:
        """Send an OPTIONS request."""

    def close(self):
        """Close the client and cleanup resources."""

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.close()

AsyncCycleTLS Class

class AsyncCycleTLS:
    def __init__(self):
        """Initialize async CycleTLS client."""

    async def request(
        self,
        method: str,
        url: str,
        params: Optional[Dict] = None,
        data: Optional[Any] = None,
        json_data: Optional[Dict] = None,
        files: Optional[Dict] = None,
        poll_interval: float = 0.0,
        timeout: float = 30.0,
        **kwargs
    ) -> Response:
        """Send an async HTTP request."""

    async def get(self, url: str, params: Optional[Dict] = None, **kwargs) -> Response:
        """Send an async GET request."""

    async def post(
        self,
        url: str,
        params: Optional[Dict] = None,
        data: Optional[Any] = None,
        json_data: Optional[Dict] = None,
        **kwargs
    ) -> Response:
        """Send an async POST request."""

    async def put(self, url: str, data: Optional[Any] = None, json_data: Optional[Dict] = None, **kwargs) -> Response:
        """Send an async PUT request."""

    async def patch(self, url: str, data: Optional[Any] = None, json_data: Optional[Dict] = None, **kwargs) -> Response:
        """Send an async PATCH request."""

    async def delete(self, url: str, **kwargs) -> Response:
        """Send an async DELETE request."""

    async def head(self, url: str, **kwargs) -> Response:
        """Send an async HEAD request."""

    async def options(self, url: str, **kwargs) -> Response:
        """Send an async OPTIONS request."""

    async def close(self):
        """Close the async client and cleanup resources."""

    async def __aenter__(self):
        """Async context manager entry."""
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit."""
        await self.close()

Usage:

import asyncio
from cycletls import AsyncCycleTLS

async def main():
    # Async context manager (recommended)
    async with AsyncCycleTLS() as client:
        response = await client.get('https://httpbin.org/get')

    # Manual lifecycle
    client = AsyncCycleTLS()
    response = await client.get('https://httpbin.org/get')
    await client.close()

asyncio.run(main())

Request Parameters

Parameter Type Description
url str Target URL (required)
method str HTTP method: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
params dict Query parameters to append to URL
data dict/str/bytes Request body (form data or raw)
json_data dict JSON request body (auto-serialized)
files dict File uploads (not yet implemented)
headers dict Custom HTTP headers
cookies dict/list/CookieJar Cookies to send with request
TLS Fingerprinting
ja3 str JA3 fingerprint string
ja4r str JA4R raw format fingerprint
http2_fingerprint str HTTP/2 fingerprint string
quic_fingerprint str QUIC fingerprint string
disable_grease bool Disable GREASE for exact JA4 matching
TLS Configuration
server_name str Custom SNI (Server Name Indication)
insecure_skip_verify bool Skip TLS certificate verification
tls13_auto_retry bool Auto-retry with TLS 1.3 compatible curves
Protocol Options
force_http1 bool Force HTTP/1.1 protocol
force_http3 bool Force HTTP/3 protocol
protocol Protocol Explicit protocol: http1, http2, http3, websocket, sse
Connection Options
user_agent str User Agent string
proxy str Proxy URL (http://, socks4://, socks5://, socks5h://)
timeout int Request timeout in seconds (default: 6)
disable_redirect bool Disable automatic redirects
enable_connection_reuse bool Enable connection pooling
Header Options
header_order list[str] Custom header ordering
order_headers_as_provided bool Use provided header order

Response Object

class Response:
    # Properties
    status_code: int        # HTTP status code
    ok: bool               # True if 200 <= status < 400
    is_redirect: bool      # True if 300 <= status < 400
    is_error: bool         # True if status >= 400
    is_client_error: bool  # True if 400 <= status < 500
    is_server_error: bool  # True if 500 <= status < 600
    reason: str            # HTTP reason phrase ("OK", "Not Found", etc.)
    url: str              # Final URL after redirects
    encoding: str         # Character encoding (detected from headers)

    text: str             # Response body as string
    content: bytes        # Response body as bytes
    headers: CaseInsensitiveDict  # Response headers
    cookies: CookieJar    # Response cookies

    # Methods
    def json(self) -> dict:
        """Parse response body as JSON."""

    def raise_for_status(self) -> None:
        """Raise HTTPError if status indicates error (>= 400)."""

Session Class

class Session(CycleTLS):
    cookies: CookieJar              # Persistent cookie jar
    headers: CaseInsensitiveDict    # Persistent headers

    def __init__(self):
        """Initialize a Session with persistent cookies/headers."""

Cookie Class

class Cookie:
    name: str
    value: str
    path: Optional[str] = None
    domain: Optional[str] = None
    expires: Optional[datetime] = None
    max_age: Optional[int] = None
    secure: bool = False
    http_only: bool = False
    same_site: Optional[str] = None  # "Strict", "Lax", or "None"

Exception Hierarchy

CycleTLSError                # Base exception
โ””โ”€โ”€ RequestException         # Base for request errors
    โ”œโ”€โ”€ HTTPError           # HTTP error (4xx, 5xx)
    โ”œโ”€โ”€ ConnectionError     # Connection failed
    โ”œโ”€โ”€ Timeout            # Request timeout
    โ”‚   โ”œโ”€โ”€ ConnectTimeout  # Connection timeout
    โ”‚   โ””โ”€โ”€ ReadTimeout     # Read timeout
    โ”œโ”€โ”€ TooManyRedirects   # Exceeded redirect limit
    โ”œโ”€โ”€ InvalidURL         # Malformed URL
    โ”œโ”€โ”€ TLSError           # TLS handshake error
    โ”œโ”€โ”€ ProxyError         # Proxy connection error
    โ””โ”€โ”€ InvalidHeader      # Invalid header value

Comparison with TypeScript Version

API Differences

Feature TypeScript Python
Initialization const client = await initCycleTLS() client = CycleTLS()
Context Manager Manual .exit() with CycleTLS() as client:
Response Parsing await response.json() response.json() (sync)
Binary Data responseType: 'stream' response.content (bytes)
Cookies Dict or Cookie array Dict or Cookie objects
Parameter Naming camelCase (userAgent) snake_case (user_agent)
Error Handling Promise rejection Python exceptions
Sessions Not built-in Session() class

Python Advantages

โœ… Simple API - Zero boilerplate module-level functions (cycletls.get(url)) โœ… Pythonic API - Context managers, properties, snake_case naming โœ… Synchronous by default - Simpler for most use cases โœ… Type hints - Full Pydantic model validation โœ… requests-like interface - Familiar to Python developers โœ… Session support - Built-in persistent cookies/headers โœ… Case-insensitive headers - Automatic via CaseInsensitiveDict โœ… Rich exceptions - Specific exception types for different errors โœ… Configurable defaults - Set once, use everywhere

Migration Example

TypeScript:

const initCycleTLS = require('cycletls');

(async () => {
  const cycleTLS = await initCycleTLS();

  const response = await cycleTLS('https://httpbin.org/post', {
    body: JSON.stringify({key: 'value'}),
    headers: {'Content-Type': 'application/json'},
    ja3: '771,4865-4866...',
    userAgent: 'Mozilla/5.0...'
  }, 'POST');

  const data = await response.json();
  console.log(data);

  await cycleTLS.exit();
})();

Python (Simple API - Recommended):

import cycletls

response = cycletls.post(
    'https://httpbin.org/post',
    json_data={'key': 'value'},
    ja3='771,4865-4866...',
    user_agent='Mozilla/5.0...'
)

data = response.json()
print(data)

Python (Manual Client):

from cycletls import CycleTLS

with CycleTLS() as client:
    response = client.post(
        'https://httpbin.org/post',
        json_data={'key': 'value'},
        ja3='771,4865-4866...',
        user_agent='Mozilla/5.0...'
    )

    data = response.json()
    print(data)

Examples

Comprehensive examples can be found in the examples/ directory:

Async Examples:

  • async_basic.py - Basic async/await usage with all HTTP methods
  • async_concurrent.py - Concurrent requests, performance comparison, rate limiting
  • async_with_fingerprinting.py - Async with JA3/JA4R fingerprinting

Sync Examples:

  • basic_request.py - Simple GET/POST requests
  • ja3_fingerprint.py - JA3 fingerprinting with multiple browsers
  • ja4_fingerprint.py - JA4R advanced fingerprinting
  • http2_fingerprint.py - HTTP/2 custom settings
  • http3_request.py - HTTP/3 and QUIC usage
  • proxy_usage.py - All proxy types with authentication
  • connection_pooling.py - Connection reuse examples
  • websocket_client.py - WebSocket communication
  • sse_client.py - Server-Sent Events handling
  • binary_upload.py - Binary data upload/download
  • form_submission.py - Form data handling
  • advanced_tls.py - Advanced TLS configuration
  • all_features.py - Comprehensive feature showcase

Testing

With uv:

# Run all tests
uv run pytest tests/

# Run specific test categories
uv run pytest tests/test_ja3_fingerprints.py
uv run pytest tests/test_http2.py
uv run pytest tests/test_cookies.py

# Run with verbose output
uv run pytest -v tests/

# Run with coverage
uv run pytest --cov=cycletls tests/

Without uv:

# Run all tests
pytest tests/

# Run specific test categories
pytest tests/test_ja3_fingerprints.py
pytest tests/test_http2.py
pytest tests/test_cookies.py

# Run with verbose output
pytest -v tests/

# Run with coverage
pytest --cov=cycletls tests/

Benchmarks

CycleTLS includes a comprehensive benchmark suite to measure performance across different configurations and compare against other HTTP libraries.

Quick Benchmark

Run a quick performance test with the CycleTLS-specific benchmark:

# Basic benchmark (default: 100 requests)
python benchmarks/benchmark_python.py --url https://httpbin.org/get

# More requests for accurate results
python benchmarks/benchmark_python.py --url https://httpbin.org/get -r 1000

# Async mode (concurrent requests)
python benchmarks/benchmark_python.py --url https://httpbin.org/get -r 1000 --async

Multi-Library Comparison

Compare CycleTLS against other popular HTTP libraries:

# Install benchmark dependencies
uv sync --extra benchmark

# Sync comparison (cycletls, requests, httpx, urllib3)
python benchmarks/bench.py --url https://httpbin.org/get -r 1000 --libraries cycletls requests httpx

# Async comparison with sync baseline
python benchmarks/bench_async.py --url https://httpbin.org/get -r 1000 --include-sync-baseline

# Generate CSV and chart output
python benchmarks/bench.py --url https://httpbin.org/get -r 1000 -o results.csv -c results.jpg

Sample Results

Performance comparison against a local Go fasthttp server (500 requests per test):

Library ยตs/req Requests/sec vs Requests
cycletls (async) 116.4 8,589 7.1x faster
primp (async) 124.4 8,038 6.6x faster
aiohttp (async) 151.7 6,591 5.4x faster
primp (sync) 157.2 6,360 5.2x faster
pycurl (reuse) 209.7 4,769 3.9x faster
curl_cffi (async) 224.2 4,459 3.7x faster
curl_cffi (sync) 298.3 3,352 2.8x faster
tls_client (sync) 309.6 3,230 2.7x faster
cycletls (sync) 346.2 2,888 2.4x faster
hrequests (sync) 398.5 2,510 2.1x faster
urllib3 (pooled) 402.9 2,482 2.0x faster
httpx (sync) 530.1 1,887 1.6x faster
niquests (session) 655.6 1,525 1.3x faster
requests (session) 824.1 1,213 baseline

Note: Results depend on hardware, network conditions, and server response times. Lower ยตs/req is better. Benchmark server: go run bench_server.go

TLS Spoofing Libraries Comparison

Feature cycletls curl_cffi tls_client primp hrequests
TLS Fingerprinting
JA3 Fingerprint โœ… โœ… โœ… โœ… โœ…
JA4 Fingerprint โœ… โŒ โŒ โœ… โŒ
HTTP/2 Fingerprint โœ… โœ… โœ… โœ… โœ…
Custom Fingerprints โœ… โœ… โœ… โœ… โŒ
Protocol Support
HTTP/2 โœ… โœ… โœ… โœ… โœ…
HTTP/3 (QUIC) โœ… โŒ โŒ โŒ โŒ
WebSocket โœ… โœ… โŒ โŒ โŒ
Server-Sent Events โœ… โŒ โŒ โŒ โŒ
Async Support
Native async/await โœ… โœ… โŒ โœ… โœ…
Concurrent requests โœ… โœ… โŒ โœ… โœ…
Other Features
Browser Profiles โœ… โœ… โœ… โœ… โœ…
Proxy (HTTP/SOCKS5) โœ… โœ… โœ… โœ… โœ…
Session/Cookies โœ… โœ… โœ… โœ… โœ…
Connection Pooling โœ… โœ… โœ… โœ… โœ…
Browser Automation โŒ โŒ โŒ โŒ โœ…
Implementation
Backend Go (uTLS) C (curl) Go Rust Go
Python Version โ‰ฅ3.8 โ‰ฅ3.9 โ‰ฅ3.7 โ‰ฅ3.8 โ‰ฅ3.8

Key Differentiators:

  • cycletls: Only library with JA4 + HTTP/3 + SSE support. Best async performance (7.1x faster than requests).
  • primp: Rust-based (rquest). Fastest sync performance (5.2x). JA4 support.
  • curl_cffi: Mature curl-impersonate bindings. Good async performance (3.7x).
  • tls_client: Simple Go-based client. No async support.
  • hrequests: Unique browser automation capability (Firefox/Chrome control).

Standard HTTP Libraries

For reference, here's how standard (non-TLS-spoofing) libraries compare:

Library ยตs/req Requests/sec TLS Spoofing
aiohttp (async) 151.7 6,591 โŒ
pycurl (reuse) 209.7 4,769 โŒ
urllib3 (pooled) 402.9 2,482 โŒ
httpx (sync) 530.1 1,887 โŒ
niquests (session) 655.6 1,525 โŒ
requests (session) 824.1 1,213 โŒ

Standard libraries cannot bypass TLS fingerprint detection but are included for performance comparison.

Programmatic Benchmarking

Run benchmarks directly in Python:

import asyncio
import time
import cycletls

def benchmark_sync(url: str, count: int = 1000):
    """Benchmark synchronous requests."""
    start = time.perf_counter()
    for _ in range(count):
        cycletls.get(url)
    elapsed = time.perf_counter() - start
    print(f"Sync: {count/elapsed:.2f} req/s")

async def benchmark_async(url: str, count: int = 1000):
    """Benchmark concurrent async requests."""
    start = time.perf_counter()
    await asyncio.gather(*[cycletls.aget(url) for _ in range(count)])
    elapsed = time.perf_counter() - start
    print(f"Async concurrent: {count/elapsed:.2f} req/s")

# Run benchmarks
url = "https://httpbin.org/get"
benchmark_sync(url, 100)
asyncio.run(benchmark_async(url, 100))

Libraries Compared

The benchmark suite can test against:

  • cycletls - This library (sync, async, session, global client)
  • requests - Popular sync HTTP library
  • httpx - Modern sync/async HTTP library
  • urllib3 - Low-level HTTP library
  • aiohttp - Async HTTP library

For detailed benchmark documentation and additional options, see benchmarks/README.md.

Development Setup

If you want to build from source:

With uv (Recommended):

# Clone repository
git clone https://github.com/Danny-Dasilva/cycletls_python.git
cd cycletls_python

# Install dependencies
uv sync                    # Install base dependencies
uv sync --all-extras       # Install with dev/docs/benchmark dependencies

# Build Go binaries
./scripts/build.sh

# Run tests
uv run pytest tests/

# Run benchmarks
uv run python benchmarks/benchmark_python.py --url https://httpbin.org/get -r 500

# Full multi-library comparison (requires benchmark extras)
uv sync --extra benchmark
uv run python benchmarks/bench.py --url https://httpbin.org/get -r 1000

Without uv:

# Clone repository
git clone https://github.com/Danny-Dasilva/cycletls_python.git
cd cycletls_python

# Install dependencies
pip install -e ".[dev]"

# Build Go binaries
./scripts/build.sh

# Run tests
pytest tests/

Building for Multiple Platforms

The scripts/build.sh script builds binaries for:

  • Linux (AMD64, ARM64)
  • macOS (AMD64, ARM64/Apple Silicon)
  • Windows (AMD64)
./scripts/build.sh

Questions

How do I use different JA3 fingerprints?
from cycletls import CycleTLS

FINGERPRINTS = {
    'chrome_120': '771,4865-4866-4867...',
    'firefox_115': '771,4865-4867-4866...',
    'safari_17': '771,4865-4867-4866...'
}

with CycleTLS() as client:
    response = client.get(
        'https://ja3er.com/json',
        ja3=FINGERPRINTS['chrome_120']
    )
How do I handle cookies across requests?

Use the Session class for automatic cookie persistence:

from cycletls import Session

with Session() as session:
    # Login
    session.post('https://example.com/login',
                 json_data={'user': 'admin', 'pass': 'secret'})

    # Cookies automatically included in subsequent requests
    profile = session.get('https://example.com/profile')
How do I download images and files?
from cycletls import CycleTLS

with CycleTLS() as client:
    response = client.get('https://httpbin.org/image/jpeg')

    # Save binary content
    with open('image.jpg', 'wb') as f:
        f.write(response.content)
How do I use SOCKS5 proxy with Tor?
from cycletls import CycleTLS

with CycleTLS() as client:
    response = client.get(
        'https://check.torproject.org',
        proxy='socks5://127.0.0.1:9050'
    )
    print(response.text)
How do I combine JA3, JA4R, and HTTP/2 fingerprints?
from cycletls import CycleTLS

with CycleTLS() as client:
    response = client.get(
        'https://tls.peet.ws/api/all',
        ja3='771,4865-4866-4867...',
        ja4r='t13d1516h2_002f,0035...',
        http2_fingerprint='1:65536;2:0;4:131072...',
        user_agent='Mozilla/5.0 ...'
    )

License

GPL3 LICENSE SYNOPSIS

TL;DR Here's what the GPL3 license entails:

1. Anyone can copy, modify and distribute this software.
2. You have to include the license and copyright notice with each and every distribution.
3. You can use this software privately.
4. You can use this software for commercial purposes.
5. Source code MUST be made available when the software is distributed.
6. Any modifications of this code base MUST be distributed with the same license, GPLv3.
7. This software is provided without warranty.
8. The software author or license can not be held liable for any damages inflicted by the software.

More information about the LICENSE can be found here

Acknowledgments

  • Based on CycleTLS by Danny Dasilva
  • Powered by uTLS for TLS fingerprinting
  • Built with Pydantic for data validation

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cycletls-0.0.3.tar.gz (625.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cycletls-0.0.3-py3-none-any.whl (5.6 MB view details)

Uploaded Python 3

File details

Details for the file cycletls-0.0.3.tar.gz.

File metadata

  • Download URL: cycletls-0.0.3.tar.gz
  • Upload date:
  • Size: 625.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for cycletls-0.0.3.tar.gz
Algorithm Hash digest
SHA256 d16b732abf9de3c1d2318135405683e80dac7df5ea4040e3359ef4c816fecaf5
MD5 d2473f077e7b0b852fa8130f89cda175
BLAKE2b-256 495dd06d069637c9e5b9684b814771e7a3ce4e8052bcc7e32c1da95959fa970d

See more details on using hashes here.

Provenance

The following attestation bundles were made for cycletls-0.0.3.tar.gz:

Publisher: release.yml on Danny-Dasilva/cycletls_python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cycletls-0.0.3-py3-none-any.whl.

File metadata

  • Download URL: cycletls-0.0.3-py3-none-any.whl
  • Upload date:
  • Size: 5.6 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for cycletls-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 c8aa5f98342fb79b3f4ab270755c531e00e01de2b106d4d21b2be09d4bdd28bb
MD5 de327c4bcc9dc83d6b287d757f17f428
BLAKE2b-256 1e8e0a11e7c29c40dab70a2d1965ad78da941ecdb6de2bc2034e21d6d9d4b5dd

See more details on using hashes here.

Provenance

The following attestation bundles were made for cycletls-0.0.3-py3-none-any.whl:

Publisher: release.yml on Danny-Dasilva/cycletls_python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page