What Is TLS Fingerprinting? A 2026 Guide

TLS fingerprinting identifies your client from its handshake, before any header is read. Here is how JA3, JA4 and JARM work, and why proxies alone will not fix it.

Author
ProxyHorizon Team
Published
July 23, 2026
10 min read
Expert-Verified
What Is TLS Fingerprinting? A [year] Guide

Your scraper sets a perfect Chrome user agent. It sends realistic headers, rotates residential IPs, and respects rate limits. It still gets blocked on the first request. What happened?

The site knew you weren't Chrome before you sent a single header. It figured that out from the TLS handshake, the encrypted greeting your client sends when it opens a connection. That greeting looks different depending on what software is making it, and anti-bot systems read it like a signature. This is TLS fingerprinting, and it's one of the most effective detection methods in use today.

Here's what this guide covers: what a TLS fingerprint actually contains, how JA3, JA4, and JARM work, why Python and Go scrapers get flagged instantly, and the honest reason your proxy budget won't save you from it. If you've ever been blocked without knowing why, this is probably the answer.

The handshake that gives you away

Before any HTTPS request happens, your client and the server negotiate encryption. Your client opens with a message called the ClientHello, which announces what it supports: TLS versions, cipher suites, extensions, elliptic curves, and more.

Here's the catch. Every TLS library builds that message differently. Chrome offers a particular set of cipher suites in a particular order. Firefox does it differently. Python's requests library, Go's net/http, and curl each have their own distinctive pattern. None of it is secret, and none of it is encrypted. It's the first thing you send.

So a server can hash those values into a short string and compare it against known clients. If your handshake matches Chrome's, you're probably Chrome. If it matches Python's, you're definitely not Chrome, no matter what your user agent claims.

How TLS fingerprinting works: a client sends a ClientHello which is turned into a fingerprint the server checks
The ClientHello becomes a fingerprint before a single header is read.

What actually goes into the fingerprint

The fields vary by method, but the classic recipe pulls from the ClientHello:

  • TLS version your client proposes
  • Cipher suites it supports, and critically, the order they're listed in
  • Extensions such as ALPN, SNI, and session tickets
  • Elliptic curves and their point formats

Order matters as much as content. Two clients can support the exact same ciphers and still produce different fingerprints because they list them differently. That ordering is baked into the library, which is why it's such a reliable signal. Developers rarely change it, and most don't know it exists.

JA3, JA4, and JARM

Three methods dominate. JA3, released by Salesforce in 2017, concatenates those ClientHello fields and hashes them with MD5. It became the industry standard almost overnight because it's simple and it works.

JA3 has a weakness though. Chrome and other browsers now shuffle their extension order on purpose (a technique called GREASE), which makes a single JA3 hash unstable across sessions. JA4, developed by FoxIO, fixes that with a longer, human-readable fingerprint that sorts fields and resists shuffling. It's steadily replacing JA3 in modern detection stacks.

JARM works the other direction. Instead of fingerprinting clients, it actively probes servers to identify what's running behind an address, which is useful for spotting malware infrastructure. Different job, same underlying idea.

MethodFingerprintsTypeStatus
JA3ClientsPassiveWidespread, aging
JA4ClientsPassiveModern replacement
JARMServersActiveSecurity research

Why your Python scraper gets caught

Detection systems don't just read your fingerprint. They cross-check it against everything else you claim to be. That's where scrapers fall apart.

Say your headers announce Chrome 120 on Windows, but your TLS handshake matches Python's urllib3. Those two facts can't both be true. No real Chrome user has ever produced that combination. The mismatch itself is the giveaway, and it's a far stronger signal than any single suspicious value.

Cloudflare, Akamai, DataDome, and PerimeterX all run this check. It's cheap, it happens before your request is even processed, and it catches the overwhelming majority of naive scrapers. If you're wondering why your requests die instantly on protected sites, read our guide on bypassing Cloudflare when scraping alongside this one.

The uncomfortable truth: most "undetectable scraper" tutorials focus on user agents and headers. Those are the easy layer. Your TLS stack is the one you can't fake by editing a dictionary of strings.
A TLS fingerprint mismatch: headers claim Chrome while the handshake reveals Python, which gets the request flagged and blocked
Headers claiming Chrome plus a Python handshake is the contradiction that gets you blocked.

Proxies won't fix a bad TLS fingerprint

This is the part that costs people the most money, so let's be blunt about it. Proxies operate at the network layer. They change which IP address the request appears to come from. They do not touch your TLS handshake at all.

Buy the cleanest residential IPs on the market, rotate them perfectly, and your Python scraper will still hand over a Python fingerprint on every single connection. You'll burn expensive bandwidth getting blocked with better-looking IPs. We've watched teams triple their proxy spend chasing a problem that lived entirely in their HTTP client.

Both layers matter, and they solve different problems. Proxies fix IP reputation and rate limits. A browser-matched TLS stack fixes client identification. Fix one and ignore the other, and the site still sees a contradiction. For the network side, our explainer on how proxies work covers what they genuinely do.

How to beat TLS fingerprinting

You have four realistic options, and they trade off effort against fidelity.

1Impersonation libraries

Tools like curl-impersonate, curl_cffi in Python, and tls-client in Go rebuild the handshake to match a real browser. You keep the speed of a plain HTTP client and lose the Python-shaped signature. For most scraping work, this is the best value per hour of effort.

2Real browsers

Playwright, Puppeteer, and Selenium drive an actual browser engine, so the TLS handshake is genuinely Chrome's or Firefox's. Nothing to fake. The cost is resources: browsers are slow and memory-hungry compared to raw HTTP, and headless mode leaks other signals you'll need to handle.

3Antidetect browsers

These go further, managing TLS, JavaScript, canvas, and hardware signals together so everything tells one consistent story. That consistency is the whole point, and it's why they dominate multi-account work. See how antidetect browsers work or browse our antidetect browser directory.

4Managed scraping APIs

Services like the scraping APIs from Oxylabs or ZenRows handle fingerprinting, proxies, and retries behind a single endpoint. You pay more per request and give up control, but you stop maintaining an arms race you didn't sign up for.

ApproachFidelitySpeedEffort
Impersonation libraryHighFastLow
Real browserPerfectSlowMedium
Antidetect browserPerfectSlowMedium
Scraping APIHandled for youVariesLowest

Our recommendation: start with curl_cffi or curl-impersonate. It fixes the fingerprint mismatch with a few lines of code and costs nothing. Move to real browsers only when the target needs JavaScript execution.

The IP layer still needs good proxies

Fixing your handshake solves identification. It does nothing for rate limits, geo-restrictions, or IP reputation, so you still want a solid proxy pool underneath. Three we rate for scraping work:

1Decodo

Pool:115M+
Uptime:99.99%
Latency:0.6s
Countries:195+
Huge 97M+ residential IP pool
Beginner-friendly dashboard and documentation
Flexible pay-as-you-go pricing
High success rates on tough targets
Fast 24/7 live chat support
Free trial and money-back guarantee

Clean residential IPs, straightforward rotation, and pricing that suits smaller projects. It's the easiest starting point when you're pairing proxies with a fixed TLS stack.

2Oxylabs

Pool:102M+
Uptime:99.99%
Latency:0.6s
Countries:195+
Massive 102M+ IP Pool
Ethically Sourced & Compliant
AI-Powered Web Unblocker
Dedicated Account Manager
Advanced ASN & City Targeting

Enterprise-grade pool plus a Web Scraper API that handles fingerprinting for you if you'd rather not maintain it yourself. Premium pricing, but it scales.

3IPRoyal

Pool:32M+
Uptime:99.9%
Latency:0.8s
Countries:195+
Traffic never expires (pay-as-you-go)
Ethically sourced residential IPs
Crypto and flexible payment options
Affordable entry pricing
Sticky sessions up to 24 hours

Non-expiring traffic makes it forgiving for bursty projects and testing. Good value while you're still tuning your setup. Compare the full field in our proxy directory.

Mistakes that get scrapers flagged

1Spoofing the user agent and nothing else

Changing the user agent string while your TLS stack screams Python creates the exact contradiction detection systems look for. You're not hiding, you're raising your hand. Match the handshake or don't bother changing the string.

2Assuming residential IPs are enough

Clean IPs help with reputation and rate limits. They don't change your fingerprint. Teams routinely spend heavily here while the real problem sits in their HTTP library.

3Forgetting HTTP/2 fingerprinting

TLS isn't the only protocol-level tell. HTTP/2 settings frames, header order, and priority values also vary by client, and sites fingerprint those too. Impersonation libraries usually handle both. Hand-rolled solutions often don't.

4Ignoring the browser layer

Get past TLS and JavaScript checks are waiting: canvas, WebGL, fonts, screen dimensions. Our guides on browser fingerprinting and canvas and WebGL fingerprinting cover what comes next.

5Chasing perfect stealth instead of consistency

You don't need to be invisible. You need every layer to tell the same story. A consistent, ordinary-looking client beats an exotic one that contradicts itself, every time.

Frequently Asked Questions

It’s a way for a server to identify what software is connecting to it by examining the TLS handshake. When your client opens an HTTPS connection it sends a ClientHello message listing the TLS versions, cipher suites, extensions, and curves it supports. Every library builds that message slightly differently, so the pattern acts like a signature. A server can hash it and compare it to known clients, which reveals whether you’re really Chrome or actually a Python script.
JA3 is a fingerprinting method released by Salesforce in 2017. It takes specific fields from the ClientHello, including the TLS version, cipher suites, extensions, elliptic curves, and curve point formats, joins them into a single string, and hashes it with MD5. The result is a short identifier that maps to a particular client. It became the industry standard because it’s simple to implement, though modern browsers that shuffle their extension order have made it less stable than it once was.
JA3 produces an MD5 hash and is sensitive to extension ordering, which browsers now randomise deliberately using a technique called GREASE. That randomisation makes a single JA3 value unreliable across sessions. JA4, developed by FoxIO, addresses this with a longer, human-readable fingerprint that sorts fields so shuffling doesn’t change the result. JA4 is also part of a broader family covering other protocols, and it’s gradually replacing JA3 in modern detection systems.
No, and this catches a lot of people out. A proxy changes the IP address your request appears to come from, but it forwards your TLS handshake unchanged, so your client fingerprint travels with it. You can use the cleanest residential IPs available and still be identified as a Python script on every connection. Proxies solve IP reputation, rate limiting, and geo-restrictions. Fixing your fingerprint requires changing the HTTP client itself.
The most practical option is an impersonation library such as curl-impersonate, curl_cffi for Python, or tls-client for Go. These rebuild the handshake so it matches a real browser while keeping the speed of a plain HTTP client. Alternatively, drive a real browser with Playwright or Puppeteer, which produces a genuine browser handshake but costs far more in resources. Managed scraping APIs handle it for you if you’d rather not maintain the setup yourself.
On its own it makes things worse. Detection systems compare what your headers claim against what your handshake reveals, so announcing Chrome while sending a Python fingerprint creates a contradiction that no genuine user could produce. That mismatch is a stronger detection signal than either value alone. If you change the user agent, change the TLS stack to match it, otherwise you’re advertising that you’re trying to hide something.
It’s standard across major anti-bot providers, including Cloudflare, Akamai, DataDome, and PerimeterX, which means any site behind those services is likely checking it. Financial services, ticketing platforms, sneaker retailers, travel sites, and large e-commerce operations are especially aggressive about it. The check is cheap and happens before your request is even processed, which is why blocked requests often fail instantly rather than after any visible challenge.
It’s a close relative that works one layer up. Where TLS fingerprinting reads the handshake, HTTP/2 fingerprinting looks at settings frames, header ordering, window sizes, and priority information, all of which differ between clients. Sites frequently check both together, so fixing only the TLS layer can still leave you exposed. Good impersonation libraries handle both, which is another reason to prefer them over hand-rolled attempts at spoofing.
Yes, because they run a real browser engine, so the handshake is genuinely a browser’s rather than something imitated. Their bigger advantage is consistency: they manage TLS, JavaScript, canvas, fonts, and hardware signals together so every layer tells the same story. That coherence is what actually defeats detection, since contradictions between layers are what get flagged. The trade-off is that they’re heavier and slower than a lightweight HTTP client.

The takeaway

TLS fingerprinting works because it reads something you can't easily lie about. Your handshake is built by your networking library, it goes out before any header you carefully crafted, and it rarely matches the browser you're pretending to be.

The fix isn't more proxies. It's making your client tell one coherent story: a browser-matched handshake, headers that agree with it, and a clean IP underneath. Start with curl_cffi or curl-impersonate, because it costs an afternoon and solves the problem for most targets. Reach for a real browser or an antidetect browser when JavaScript checks enter the picture.

Once your fingerprint stops contradicting itself, the proxy layer finally does what you're paying it to do. Compare pools in our proxy directory, or read up on why scraping needs proxies to get the network side right too.