What Is TLS Fingerprinting?
TLS fingerprinting is a passive detection technique that identifies clients based on how they initiate encrypted connections. Every time your scraper, browser, or HTTP library connects to a website over HTTPS, it sends a TLS ClientHello message containing cipher suites, extensions, elliptic curves, and other parameters in a specific order. Anti-bot systems analyze this handshake to determine whether the connecting client matches what its user-agent claims to be.
Unlike browser fingerprinting, which requires JavaScript execution, TLS fingerprinting works at the network layer — before any page content is delivered. This makes it one of the earliest and most difficult detection signals to evade, as covered in our comprehensive guide to how anti-bot systems detect proxies.
How TLS Handshakes Work
Before any HTTP data is exchanged over HTTPS, the client and server perform a TLS handshake. The critical first message — the ClientHello — contains everything anti-bot systems need for fingerprinting:
- TLS version: The maximum TLS version the client supports (e.g., TLS 1.2, TLS 1.3).
- Cipher suites: An ordered list of encryption algorithms the client is willing to use.
- Extensions: Additional capabilities such as Server Name Indication (SNI), ALPN, signature algorithms, and key share groups.
- Elliptic curves: Supported curve types for key exchange (e.g., x25519, secp256r1).
- Compression methods: Typically null in modern implementations, but their presence or absence is still a signal.
Each HTTP library, browser, and programming language runtime produces a distinct ClientHello pattern. Chrome, Firefox, Safari, Python's requests, Go's net/http, and Node.js each have recognizable signatures.
JA3 Fingerprinting
JA3 is the most widely deployed TLS fingerprinting method. Developed by Salesforce engineers, it creates an MD5 hash from five fields in the ClientHello message:
| Field | Description | Example Values |
|---|---|---|
| TLS Version | The protocol version offered | 771 (TLS 1.2), 772 (TLS 1.3) |
| Cipher Suites | Ordered list of cipher suite codes | 4865-4866-4867-49195-49199... |
| Extensions | List of extension type codes | 0-23-65281-10-11-35-16-5... |
| Elliptic Curves | Supported named groups | 29-23-24 |
| EC Point Formats | Supported point format types | 0 |
These five values are concatenated with commas and hashed to produce a 32-character JA3 fingerprint. For example, Python's requests library produces a different JA3 hash than Chrome, even when both set the same user-agent string.
JA3 Detection in Practice
# Example JA3 hash computation (conceptual)
# ClientHello fields → concatenated string → MD5 hash
# Python requests (urllib3/OpenSSL) — distinct JA3
# ja3: 771,4866-4867-4865-49196-49200-159-52393-52392-52394...,0-23-65281-10-11...
# ja3_hash: "773906b0efdefa24a7f2b8eb6985bf37"
# Chrome 120+ — different cipher order, different extensions
# ja3: 771,4865-4866-4867-49195-49199-49196-49200-52393-52392...,0-23-65281-10-11...
# ja3_hash: "cd08e31494f9531f560d64c695473da9"
# The hash reveals the client library, regardless of User-Agent
JA4 — The Next Generation
JA4, also from Salesforce, improves on JA3 by producing a more readable and robust fingerprint. Instead of an opaque MD5 hash, JA4 creates a structured identifier with three components:
- JA4_a: Protocol type + TLS version + SNI presence + cipher count + extension count + ALPN first value (e.g., "t13d1517h2_8daaf6152771_b0da82dd1658").
- JA4_b: Sorted truncated hash of cipher suites.
- JA4_c: Sorted truncated hash of extensions (with SNI and ALPN removed to reduce variability).
JA4 is harder to spoof because it incorporates additional signals and uses a format that resists simple hash matching.
Common TLS Fingerprints by Client
| Client | TLS Library | Recognizable Traits | Detection Risk |
|---|---|---|---|
| Chrome (latest) | BoringSSL | Specific cipher order, GREASE values, ECH support | Low (if matched correctly) |
| Firefox | NSS | Different cipher preference, delegated credentials extension | Low (if matched correctly) |
| Python requests | OpenSSL (via urllib3) | Missing GREASE, fewer extensions, OpenSSL cipher order | Very High |
| Go net/http | Go crypto/tls | Unique cipher order, missing many extensions | Very High |
| Node.js (axios/got) | OpenSSL (via Node) | Node-specific extension order, missing GREASE | High |
| curl | Varies (OpenSSL/NSS/etc.) | Depends on build, but typically non-browser fingerprint | High |
Why TLS Fingerprinting Is Hard to Evade
TLS fingerprinting presents unique challenges compared to other detection methods:
- Network-layer detection: It operates before any HTTP content is exchanged, so it cannot be defeated by JavaScript injection or header manipulation.
- Library-level signature: The fingerprint is determined by the TLS library compiled into your runtime, not by your application code. Changing the user-agent string has zero effect on the TLS fingerprint.
- Proxy transparency: Standard HTTP/HTTPS proxies (including residential proxies) forward the TLS handshake from the client to the server, so the origin sees your client's real TLS fingerprint.
- Version coupling: Each minor version of a TLS library can produce a slightly different fingerprint, making version mismatches detectable.
TLS Fingerprinting Mitigation Strategies
1. Use Browser-Grade TLS Libraries
The most effective approach is to use TLS libraries that produce browser-identical ClientHello messages:
# Python: Use curl_cffi to mimic browser TLS fingerprints
# pip install curl_cffi
from curl_cffi import requests
# Impersonate Chrome's TLS fingerprint
response = requests.get(
"https://example.com",
impersonate="chrome",
proxies={
"http": "http://USERNAME:PASSWORD@gate.proxyhat.com:8080",
"https": "http://USERNAME:PASSWORD@gate.proxyhat.com:8080"
}
)
print(response.status_code)
2. Use utls in Go
// Go: Use uTLS to mimic browser TLS fingerprints
// go get github.com/refraction-networking/utls
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"crypto/tls"
tls2 "github.com/refraction-networking/utls"
)
func main() {
proxyURL, _ := url.Parse("http://USERNAME:PASSWORD@gate.proxyhat.com:8080")
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
}
// uTLS allows you to specify a ClientHelloID that mimics
// specific browsers (Chrome, Firefox, Safari, etc.)
// This requires custom dial integration — see uTLS docs
_ = tls2.HelloChrome_Auto // Example: mimic Chrome
client := &http.Client{Transport: transport}
resp, err := client.Get("https://example.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body[:100]))
}
3. Use Node.js with Custom TLS
// Node.js: Use got-scraping for browser-like TLS
// npm install got-scraping
import { gotScraping } from 'got-scraping';
const response = await gotScraping({
url: 'https://example.com',
proxyUrl: 'http://USERNAME:PASSWORD@gate.proxyhat.com:8080',
headerGeneratorOptions: {
browsers: ['chrome'],
operatingSystems: ['windows'],
}
});
// got-scraping uses custom TLS settings to mimic browser fingerprints
console.log(response.statusCode);
4. Use Headless Browsers
Headless browsers (Puppeteer, Playwright) produce authentic browser TLS fingerprints because they use the real browser TLS stack. This is the most reliable mitigation but also the most resource-intensive. See our guide to scraping without getting blocked for setup details.
Testing Your TLS Fingerprint
Before deploying your scraper, verify its TLS fingerprint against detection services:
# Check your JA3 fingerprint against a test service
# Using Python with curl_cffi
from curl_cffi import requests
response = requests.get(
"https://tls.peet.ws/api/all",
impersonate="chrome",
proxies={
"http": "http://USERNAME:PASSWORD@gate.proxyhat.com:8080",
"https": "http://USERNAME:PASSWORD@gate.proxyhat.com:8080"
}
)
data = response.json()
print(f"JA3 Hash: {data.get('tls', {}).get('ja3_hash', 'N/A')}")
print(f"JA4: {data.get('tls', {}).get('ja4', 'N/A')}")
print(f"HTTP Version: {data.get('http_version', 'N/A')}")
Your TLS fingerprint is determined by your HTTP client library, not by your proxy. Switching from datacenter to residential proxies changes your IP reputation but does not change your TLS signature. Both layers must be addressed.
HTTP/2 Fingerprinting
Beyond TLS, the HTTP/2 protocol itself reveals client identity through connection settings, header frame order, and priority frames. Anti-bot systems combine TLS and HTTP/2 fingerprints for higher accuracy:
| HTTP/2 Signal | What It Reveals |
|---|---|
| SETTINGS frame values | Initial window size, max concurrent streams — differs by client |
| WINDOW_UPDATE size | Flow control increment value — unique to each implementation |
| Header frame order | Pseudo-header ordering (:method, :authority, :scheme, :path) |
| PRIORITY frames | Stream dependency and weight — browser-specific patterns |
Libraries like curl_cffi and got-scraping address HTTP/2 fingerprinting in addition to TLS fingerprinting.
Combining TLS Mitigation with Proxy Rotation
An effective anti-detection strategy layers TLS fingerprint matching with high-quality proxy rotation:
- Match TLS to user-agent: If your user-agent claims Chrome, your TLS fingerprint must match Chrome.
- Use residential proxies: ProxyHat's residential proxies provide clean IPs that complement browser-grade TLS signatures.
- Rotate consistently: Each session should use a matching IP + TLS profile + user-agent combination.
- Avoid mixing libraries: Do not reuse the same IP with different TLS fingerprints — this is a strong bot signal.
- Test before deployment: Verify your fingerprint matches your claimed browser using test endpoints.
For language-specific proxy integration, see our guides for Python, Node.js, and Go.
Ethical and Legal Considerations
TLS fingerprint mimicry should be used responsibly. Legitimate use cases include:
- Accessing publicly available data through standard HTTPS connections
- Security research and penetration testing of your own infrastructure
- Ensuring your automated tests accurately simulate real browser behavior
- Privacy research studying how TLS fingerprinting affects user tracking
Always respect website terms of service, rate limits, and applicable regulations. Refer to ProxyHat's documentation for responsible usage guidelines.






