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
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
- Installation
- Quick Start
- Usage
- API Reference
- Comparison with TypeScript Version
- Examples
- Testing
- Benchmarks
- License
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 intervaltimeout=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 requestcycletls.apost()- Async POST requestcycletls.aput()- Async PUT requestcycletls.apatch()- Async PATCH requestcycletls.adelete()- Async DELETE requestcycletls.ahead()- Async HEAD requestcycletls.aoptions()- Async OPTIONS requestcycletls.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 proxyhttps://- HTTPS proxysocks4://- SOCKS4 proxysocks5://- SOCKS5 proxysocks5h://- 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 methodsasync_concurrent.py- Concurrent requests, performance comparison, rate limitingasync_with_fingerprinting.py- Async with JA3/JA4R fingerprinting
Sync Examples:
basic_request.py- Simple GET/POST requestsja3_fingerprint.py- JA3 fingerprinting with multiple browsersja4_fingerprint.py- JA4R advanced fingerprintinghttp2_fingerprint.py- HTTP/2 custom settingshttp3_request.py- HTTP/3 and QUIC usageproxy_usage.py- All proxy types with authenticationconnection_pooling.py- Connection reuse exampleswebsocket_client.py- WebSocket communicationsse_client.py- Server-Sent Events handlingbinary_upload.py- Binary data upload/downloadform_submission.py- Form data handlingadvanced_tls.py- Advanced TLS configurationall_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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d16b732abf9de3c1d2318135405683e80dac7df5ea4040e3359ef4c816fecaf5
|
|
| MD5 |
d2473f077e7b0b852fa8130f89cda175
|
|
| BLAKE2b-256 |
495dd06d069637c9e5b9684b814771e7a3ce4e8052bcc7e32c1da95959fa970d
|
Provenance
The following attestation bundles were made for cycletls-0.0.3.tar.gz:
Publisher:
release.yml on Danny-Dasilva/cycletls_python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cycletls-0.0.3.tar.gz -
Subject digest:
d16b732abf9de3c1d2318135405683e80dac7df5ea4040e3359ef4c816fecaf5 - Sigstore transparency entry: 851673633
- Sigstore integration time:
-
Permalink:
Danny-Dasilva/cycletls_python@27245f2ecd0639bcd393b5edf6f819bb073ca984 -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/Danny-Dasilva
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@27245f2ecd0639bcd393b5edf6f819bb073ca984 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8aa5f98342fb79b3f4ab270755c531e00e01de2b106d4d21b2be09d4bdd28bb
|
|
| MD5 |
de327c4bcc9dc83d6b287d757f17f428
|
|
| BLAKE2b-256 |
1e8e0a11e7c29c40dab70a2d1965ad78da941ecdb6de2bc2034e21d6d9d4b5dd
|
Provenance
The following attestation bundles were made for cycletls-0.0.3-py3-none-any.whl:
Publisher:
release.yml on Danny-Dasilva/cycletls_python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cycletls-0.0.3-py3-none-any.whl -
Subject digest:
c8aa5f98342fb79b3f4ab270755c531e00e01de2b106d4d21b2be09d4bdd28bb - Sigstore transparency entry: 851673679
- Sigstore integration time:
-
Permalink:
Danny-Dasilva/cycletls_python@27245f2ecd0639bcd393b5edf6f819bb073ca984 -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/Danny-Dasilva
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@27245f2ecd0639bcd393b5edf6f819bb073ca984 -
Trigger Event:
push
-
Statement type: