Most developers starting with web scraping reach for the requests library in Python or the standard http package in Go. Within minutes of hitting a protected endpoint like Cloudflare, Akamai, or DataDome, they receive a 403 Forbidden or a 429 Too Many Requests. They rotate their proxies, change their User-Agent to match a recent Chrome version, and still get blocked.
The reason isn't your IP or your User-Agent. It's your TLS fingerprint.
When your script initiates a TLS handshake, it sends a ClientHello packet. This packet contains a list of supported cipher suites, extensions, elliptic curves, and signature algorithms. The combination of these values is unique to the library you use. Python's requests uses urllib3, which uses the system's OpenSSL. Go's http.Client uses its own internal crypto/tls implementation.
Anti-bot systems map these combinations to known libraries. If your User-Agent says "Chrome 133" but your TLS handshake looks like "OpenSSL 3.0.2," you're flagged as a bot before a single byte of HTTP data is even processed.
This guide covers tls-client, a powerful library built by Bogdan Finn that allows your scripts to mimic the exact TLS and HTTP/2 fingerprints of modern browsers. We'll explore why standard libraries fail, how the 2026 JA4 standard changes the game, and provide production-ready code for Python, Go, and Node.js.
How TLS Fingerprinting Works
TLS fingerprinting identifies the client based on the parameters it negotiates during the initial handshake. Before any encrypted communication happens, the client must tell the server what it's capable of. This exchange happens in the clear, making it a perfect signal for anti-bot vendors to inspect.
The ClientHello Packet: The Digital Passport
The ClientHello is the first message sent by a client. It includes:
- TLS Version: The highest version the client supports (usually 1.2 or 1.3).
- Cipher Suites: A list of encryption algorithms the client prefers.
- Extensions: Additional features like Server Name Indication (SNI), Supported Groups, and ALPN.
- Elliptic Curves: Specific curves for key exchange.
- Signature Algorithms: Algorithms used for digital signatures.
An anti-bot like Cloudflare or Akamai doesn't just look at the presence of these fields; it looks at their order, their values, and their specific combinations. A real browser has a very specific set of supported ciphers that changes with every release. If you're using a library that hasn't updated its cipher list in three years, you're an easy target for blocking.
JA3: The Industry Standard
In 2017, Salesforce engineers introduced JA3. It creates a hash of five specific fields from the ClientHello:
TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats
For example, a typical Go 1.22 client might have a JA3 like 771,4865-4866-4867-49195-49199...,.... An anti-bot system compares this hash against a database of known browser JA3s. If there's a mismatch between the JA3 and the declared User-Agent, the connection is dropped.
JA4: The 2026 Shift
While JA3 was revolutionary, it had flaws. Google Chrome started randomizing the order of extensions (GREASE) to prevent servers from relying on a specific order. This caused "JA3 churn," where a single Chrome version could produce thousands of different hashes.
JA4 solves this by sorting extensions and ciphers alphabetically before hashing. It also includes more granular information about the protocol. The tls-client library handles these complexities by using utls (unforked TLS), which allows for low-level manipulation of the handshake to satisfy both JA3 and JA4 checks. As we move into 2026, JA4 is becoming the primary signal used by top-tier anti-bot solutions.
Architecture: Under the Hood
Bogdan Finn's tls-client is written in Go and stands on two main pillars:
- utls: A Go library that provides a way to mimic the TLS handshakes of other libraries and browsers. It allows the developer to construct a
ClientHellothat is bit-for-bit identical to a target browser. - fhttp: A fork of Go's standard
net/httplibrary that allows for precise control over HTTP/2 settings, such as header order and initial window size.
In the browser, HTTP/2 pseudo-headers (like :method, :path, :authority, :scheme) appear in a specific order. Standard HTTP libraries often change this order or use different default window sizes, which anti-bots detect. tls-client ensures that your HTTP/2 frames look identical to a real browser's frames.
The library also handles the complexities of TLS 1.3, including Encrypted Client Hello (ECH) and Pre-Shared Key (PSK) extensions, which are increasingly used by modern websites to protect against MITM attacks and fingerprinting.
Installation
Python
The Python wrapper for tls-client uses FFI (Foreign Function Interface) to call the compiled Go library. This gives you the speed of Go with the ease of Python.
pip install tls-client
Note: When you install this, it downloads a .so (Linux), .dll (Windows), or .dylib (macOS) file. Ensure your architecture matches (x64 vs ARM64). This is a common point of failure in CI/CD pipelines.
Go
Add it to your project using the standard Go toolchain:
go get github.com/bogdanfinn/tls-client
Node.js
Use the node-tls-client package, which provides a clean JavaScript API for the underlying Go binary:
npm install node-tls-client
Browser Profile Selection
Choosing the right profile is critical. You must match the TLS profile with the corresponding User-Agent header. If you use a Chrome 144 profile with a Firefox User-Agent, you will be blocked.
| Profile Name | Target Environment | Key Features |
|---|---|---|
chrome_133 | Modern Desktop Chrome | Standard H2 settings, GREASE extensions |
chrome_144_PSK | Latest Chrome (2026) | Pre-Shared Key support for TLS 1.3 |
firefox_135 | Modern Firefox | Specific H2 priority settings |
safari_ios_18_5 | Mobile Safari | Mobile-specific cipher suites |
nike_ios_mobile | Nike App (iOS) | Hardcoded native app fingerprint |
zalando_android | Zalando App (Android) | Android-specific TLS patterns |
Sites like Cloudflare often require PSK (Pre-Shared Key) profiles for their most aggressive "Under Attack" modes. This mimics the session resumption behavior of real browsers, which is a key signal for distinguishing humans from bots.
Implementation: Python Guide
In Python, tls-client provides a Session object similar to requests.Session(). This makes it easy to integrate into existing codebases.
import tls_client
# 1. Initialize the session with a specific browser profile
# We use chrome_144, the latest stable profile for 2026.
session = tls_client.Session(
client_identifier="chrome_144",
random_tls_extension_order=True
)
# 2. Define your proxies (Integration with ProxyLabs)
# Using gate.proxylabs.app ensures high-quality residential traffic.
proxies = {
"http": "http://your-username:[email protected]:8080",
"https": "http://your-username:[email protected]:8080"
}
# 3. SET THE USER-AGENT MANUALLY
# This is a common pitfall. The library does NOT set this for you.
# The User-Agent MUST match the client_identifier profile.
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
}
# 4. Make the request
# We target nowsecure.nl, a common benchmark for anti-bot bypass.
response = session.get(
"https://nowsecure.nl",
headers=headers,
proxies=proxies
)
print(f"Status Code: {response.status_code}")
print(f"Server: {response.headers.get('Server')}")
Implementation: Go Guide
The Go implementation is the most performant and offers the most control. Since the library is native to Go, there is no FFI overhead.
package main
import (
"fmt"
"io"
"log"
"github.com/bogdanfinn/tls-client"
"github.com/bogdanfinn/tls-client/profiles"
)
func main() {
// 1. Set up options
// Using WithClientProfile ensures the TLS handshake matches Chrome 144.
options := []tls_client.HttpClientOption{
tls_client.WithClientProfile(profiles.Chrome_144),
tls_client.WithNotFollowRedirects(),
// ProxyLabs residential proxy integration
tls_client.WithProxyUrl("http://your-username:[email protected]:8080"),
}
// 2. Create the client
// We use NoopLogger to keep the output clean.
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
// 3. Create request
req, _ := tls_client.NewRequest("GET", "https://httpbin.org/headers", nil)
// 4. Manually set headers to match profile
// Consistency between TLS and HTTP headers is mandatory.
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
// 5. Execute
resp, err := client.Do(req)
if err != nil {
log.Fatalf("Request failed: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Status: %d\nBody: %s\n", resp.StatusCode, body)
}
Implementation: Node.js Guide
In Node.js, we use a wrapper that manages the Go binary. This is often the preferred choice for scrapers running on serverless platforms.
const tlsClient = require('node-tls-client');
async function scrape() {
// Initialize the session with a desktop Chrome profile
const session = new tlsClient.Session({
clientIdentifier: 'chrome_144',
allowInsecure: false
});
const options = {
// Integrate ProxyLabs residential proxies
proxy: 'http://your-username:[email protected]:8080',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Connection': 'keep-alive'
}
};
try {
const response = await session.get('https://www.google.com', options);
console.log('Status Code:', response.status);
console.log('Response Headers:', response.headers);
} catch (error) {
console.error('Scraping Error:', error);
}
}
scrape();
Advanced: Custom JA3 and HTTP/2 Overrides
Sometimes a built-in profile isn't enough. You might need to replicate a specific fingerprint found in the wild from a niche browser or a specific mobile device. tls-client allows you to pass a raw JA3 string and custom HTTP/2 settings.
# Custom JA3 and HTTP/2 settings in Python
session = tls_client.Session(
client_identifier="chrome_131", # Use this as a base for H2 settings
ja3_string="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-21,29-23-24,0",
h2_settings={
"HEADER_TABLE_SIZE": 65536,
"MAX_CONCURRENT_STREAMS": 1000,
"INITIAL_WINDOW_SIZE": 6291456,
"MAX_FRAME_SIZE": 16384,
"MAX_HEADER_LIST_SIZE": 262144
},
h2_settings_order=[
"HEADER_TABLE_SIZE",
"MAX_CONCURRENT_STREAMS",
"INITIAL_WINDOW_SIZE",
"MAX_FRAME_SIZE",
"MAX_HEADER_LIST_SIZE"
]
)
By providing the ja3_string and h2_settings, you can bypass even the most restrictive signature-based blocking that relies on detecting non-standard HTTP/2 window sizes or header table limits.
Session Management and CookieJar
Bypassing TLS is only the first step. To pass Cloudflare "Turnstile" or "Waiting Rooms," you often need to maintain a persistent session. Anti-bots look for the presence of session cookies like cf_clearance. If you perform a handshake for every request without sending back the cookies you were given, you look like a script.
tls-client includes a built-in CookieJar. When you make a request to a site that sets a cookie, subsequent requests using the same session object will automatically include that cookie.
# Initial request to solve a challenge or get a session cookie
# We use a residential proxy from ProxyLabs to ensure the initial request succeeds
session.get("https://target-site.com", headers=headers, proxies=proxies)
# The second request carries the cookies from the first one
# This allows us to access protected API endpoints
protected_data = session.get("https://target-site.com/api/v1/user/profile", headers=headers, proxies=proxies)
Critical Pitfalls to Avoid
1. The User-Agent Mismatch
This is the number one reason for failure. If your client_identifier is chrome_144 but your headers specify User-Agent: Mozilla/5.0 ... Firefox/135.0, the backend logic of the anti-bot will immediately flag the contradiction. Modern anti-bots compare the JA3/JA4 hash against the User-Agent string in real-time.
2. Header Ordering (HTTP/1.1 vs HTTP/2)
In HTTP/2, the order of pseudo-headers (headers starting with a colon) is fixed by the browser. Chrome, Firefox, and Safari all use different orders. tls-client handles the pseudo-headers, but you should still provide standard headers in a logical order (User-Agent, then Accept, then Accept-Language, etc.) to mimic a browser's behavior for HTTP/1.1 fallback.
3. Binary Compatibility in Serverless
If you are deploying in a Docker container or a cloud function (like AWS Lambda or Google Cloud Functions), you must ensure that the .so or .dll file included in the Python/Node package matches the execution environment. Using an x64 binary on an ARM64 AWS Graviton instance will result in an "OSError: Exec format error." Always test your deployment package in an environment that matches your production target.
4. Native App Profiles for Mobile Scraping
When scraping mobile APIs, do not use a Chrome desktop profile. Use nike_ios_mobile or zalando_android. These profiles omit certain TLS extensions and cipher suites that are never present in mobile apps. Native apps use different TLS stacks than mobile browsers (like boringssl or ok-http), and using a browser fingerprint for an app API is a massive red flag.
Comparison: tls-client vs standard libraries
| Feature | Standard requests | tls-client |
|---|---|---|
| JA3/JA4 Fingerprint | Fixed (OpenSSL) | Customizable (Browser-identical) |
| HTTP/2 Support | Basic (via h2) | Advanced (with custom settings) |
| Header Ordering | Randomized/Alphabetical | Browser-mimicking |
| Cookie Management | Manual/Basic | Built-in Session CookieJar |
| Performance | High | High (Native Go core) |
| Ease of Use | Very Easy | Easy (Requests-like API) |
When tls-client Isn't Enough
While tls-client is exceptional at bypassing network-level fingerprinting, it is not a silver bullet. Modern security suites use a multi-layered approach:
- JavaScript Execution:
tls-clientcannot execute JS. If a site requires solving a complex mathematical puzzle in the browser, you'll need to solve it elsewhere and pass the token totls-client. - Canvas/WebGL Fingerprinting: These occur in the browser environment and require a real or virtual DOM.
- Behavioral Analysis: If your script accesses 10,000 pages in 10 minutes from the same IP, no amount of TLS spoofing will save you.
In these cases, you may need to use a headless browser like Playwright with stealth plugins or an antidetect browser. However, for 90% of API-based scraping and high-volume data extraction, tls-client combined with high-quality residential proxies from ProxyLabs is the most efficient and cost-effective solution.
Conclusion
TLS fingerprinting is a powerful tool in the anti-bot arsenal, but it's not invincible. By understanding the components of the ClientHello and utilizing tls-client to mimic real browser behavior, you can significantly increase your success rates and reduce the need for expensive headless browser clusters.
Success in modern web scraping requires a deep understanding of the protocols you use. By mastering tls-client and pairing it with ProxyLabs' global residential proxy network, you gain the ability to appear as a legitimate user from any country, using any browser, at any time.
Ready to try the fastest residential proxies?
Join developers and businesses who trust ProxyLabs for mission-critical proxy infrastructure.
Building proxy infrastructure since 2019. Previously failed at many things, now failing slightly less.
Related Articles
JA4+ TLS Fingerprinting: What Changed in 2026 and Why JA3 Is No Longer Enough
JA3 is broken by Chrome's extension randomization. JA4+ solves this with sorted, randomization-resistant fingerprints. How JA4 parts A/B/C work, HTTP/2 SETTINGS frame detection, TLS Greasing, and what actually passes anti-bot checks in 2026.
14 min readHTTP Header Order: Why It Gets You Blocked and How to Fix It
Anti-bot systems check more than just your User-Agent. HTTP/2 pseudo-header order, Sec-Ch-Ua Client Hints, and header consistency with your TLS fingerprint all matter in 2026.
9 min readContinue exploring
Implementation guides for requests, Scrapy, Axios, Puppeteer, and more.
See how residential proxies fit large-scale scraping workflows.
Evaluate ProxyLabs against Bright Data, Oxylabs, Smartproxy, and others.
Browse location coverage and targeting options across 195+ countries.