TLS fingerprinting identifies the software that opens an HTTPS connection by looking at how it opens it — which ciphers it offers and in what order, which extensions and elliptic curves it lists — before any request content is sent. Every TLS library and every browser version says hello to a server slightly differently, and that greeting is visible to the server in plaintext even though everything after it is encrypted. JA3 and its successor JA4 turn the greeting into a short hash. Anti-bot systems compare that hash against what the claimed browser should have produced. This is why a Python script announcing itself as Chrome, connecting through a flawless residential IP, is still caught: the IP said “home user in Berlin”; the TLS handshake said “OpenSSL in a script.”
Where the fingerprint comes from: the ClientHello
An HTTPS connection begins with a TLS handshake. The very first message, sent by the client, is the ClientHello. It is unencrypted — it must be, because encryption has not been negotiated yet — and it contains, among other things:
- the TLS version the client supports;
- the list of cipher suites it is willing to use, in its order of preference;
- a list of extensions — SNI (the hostname), ALPN (which application protocols, e.g.
h2,http/1.1), supported groups (elliptic curves), signature algorithms, key share, session ticket, and so on — again in a specific order; - the elliptic curves and point formats it supports.
None of these values is secret and none is chosen per site; they are baked into the TLS library and the application that uses it. Chrome 128 sends one particular ClientHello; Firefox sends another; Python's ssl module (OpenSSL) sends a third; Go's crypto/tls a fourth; curl a fifth. The server sees the ClientHello for every connection anyway — it has to, to answer it. Recording and hashing it costs nothing.
JA3 and JA4: turning the handshake into a signature
JA3
Introduced by Salesforce researchers in 2017, JA3 concatenates five fields of the ClientHello — TLS version, cipher suites, extensions, elliptic curves, elliptic-curve point formats — as comma-separated decimal lists, then MD5-hashes the string. The result is a 32-character fingerprint such as cd08e31494f9531f560d64c695473da9. Anyone can compute it from a packet capture, and lists mapping JA3 hashes to known clients (“this hash = Chrome 120 on Windows,” “this hash = python-requests”) are widely shared.
JA3 has a weakness that became a feature for evasion: because it hashes the extension list in order, and Chrome (since version 110) randomises the order of extensions on every connection, a single Chrome install produces many different JA3 hashes. That was done deliberately to discourage servers from ossifying on one fingerprint — and it made JA3 much less useful for identifying browsers.
JA4
JA4, released by FoxIO in 2023, was designed to survive that. It sorts the cipher and extension lists before hashing (so Chrome's shuffling no longer matters), keeps some fields human-readable, and folds in more signals: whether the connection is over TCP or QUIC, the SNI type, the ALPN value, TLS version, and counts of ciphers and extensions. A JA4 fingerprint looks like t13d1516h2_8daaf6152771_e5627efa2ab1: the first block says “TLS 1.3 over TCP, 15 ciphers, 16 extensions, ALPN h2,” the other two are truncated hashes of the sorted cipher list and the sorted extension list plus signature algorithms. JA4 is actually a family (JA4 for TLS, JA4H for HTTP headers, JA4S for the server side, JA4X for certificates, and others); when anti-bot vendors say “TLS fingerprint” today they usually mean JA4 or a proprietary equivalent built the same way.
Why the proxy cannot help
A proxy relays the TLS handshake; it does not rewrite it. With an HTTP proxy the client sends CONNECT host:443, the proxy opens a TCP tunnel, and the client's own ClientHello travels through the tunnel to the destination untouched. With SOCKS5 the same thing happens one layer down. The destination server therefore sees two independent facts:
- the source IP — the proxy's, residential and spotless;
- the ClientHello — the client's, saying exactly what library built it.
Anti-bot logic then asks a simple question: does the TLS fingerprint match the User-Agent? The User-Agent header claims “Chrome 128 on Windows.” The JA4 says “OpenSSL 3.x via Python.” Real Chrome never produces that JA4. The request is flagged — not because the IP is bad, but because the story is internally inconsistent. This is the single most common reason a “clean” residential proxy still gets CAPTCHAs on protected sites, and it is why we say a proxy answers where from, not who; see what a proxy does and does not hide.
The proxy can matter in one respect: some providers terminate TLS themselves (a “man-in-the-middle” proxy that decrypts and re-encrypts, requiring you to install its certificate). In that case the destination sees the proxy's ClientHello, not yours — which is usually worse, since it is a server-side library, not a browser. Ordinary proxies do not do this.
What each kind of client looks like
| Client | What its TLS fingerprint says | Consistent with a browser User-Agent? |
|---|---|---|
| Real Chrome / Firefox / Safari | That browser and version, on that OS | Yes — it is the browser |
Python requests, httpx, aiohttp | OpenSSL as configured by Python's ssl module | No — recognisable as a script regardless of headers |
Node.js fetch / axios | Node's OpenSSL/BoringSSL build | No |
Go net/http | Go's own TLS stack; distinctive | No |
| curl (default build) | OpenSSL/LibreSSL as built | No |
| Headless Chrome via Puppeteer/Playwright | Real Chrome — correct TLS | Yes for TLS; other signals (automation flags, missing plugins) may still leak |
| “Impersonating” clients (curl-impersonate, curl_cffi, tls-client) | A chosen browser's ClientHello, reproduced byte-for-byte | Yes, if you also match the headers to the same browser |
How to have a consistent fingerprint
There are three honest routes, in increasing order of cost.
- Use a real browser. Automate Chrome or Firefox with Playwright or Puppeteer; the TLS is then genuinely the browser's. Slower and heavier than raw HTTP, but for protected sites it is the baseline. Pair each browser context with its own proxy — see proxies in Playwright, Puppeteer and Selenium.
- Use an impersonating HTTP client. Libraries such as curl_cffi (Python, built on curl-impersonate) and tls-client reproduce specific browsers' ClientHello and HTTP/2 settings, so you get scraper speed with a browser's fingerprint. You must then keep all the other headers consistent with the same browser version — User-Agent,
Accept-Language,sec-ch-uaclient hints — or you swap one contradiction for another. Our Python proxy guide shows the switch fromrequeststocurl_cffi. - Stop pretending. If a target offers an official API, use it with your real client identity. TLS fingerprinting is only a problem when the fingerprint contradicts the claim.
What does not work: setting a Chrome User-Agent on requests. It changes one header and leaves the handshake untouched — the mismatch is now sharper than before, because a plain python-requests UA with an OpenSSL fingerprint is at least self-consistent.
The bigger picture: fingerprints stack
TLS is one layer. Modern anti-bot vendors also fingerprint the HTTP/2 connection (SETTINGS frame values, window sizes, header order and pseudo-header order — sometimes called Akamai fingerprinting), the TCP/IP stack (window size, TTL, options; classic p0f), and of course the browser itself through JavaScript, which an HTTP client cannot run at all. Each layer must tell the same story as the others and as the IP. A residential IP with a browser TLS fingerprint and browser HTTP/2 settings but no JavaScript execution is still an anomaly on a site that expects the JS challenge to run. This is why the honest answer to “which proxy avoids detection?” is “none by itself” — and why our rankings weigh what the provider does beyond the IP; see the elite proxy comparison.
How to see your own fingerprint
Several public pages echo the JA3/JA4 they compute for your connection, along with what browser they think it belongs to. Load one from a real browser, then fetch it with your script through the same proxy, and compare. If the two differ, you have found the reason your script is treated differently from your browser — and no proxy change will fix it. Add this to the checks in How to test a proxy whenever the target is known to run enterprise anti-bot.
Frequently asked questions
What is JA3?
An MD5 hash of five fields from the TLS ClientHello (version, ciphers, extensions, curves, point formats) used to identify the client software. It works well for scripts and libraries; it became unreliable for Chrome once Chrome began randomising extension order.
What is JA4?
The successor to JA3: it sorts the lists before hashing so extension shuffling does not change it, keeps part of the fingerprint readable, and includes transport (TCP/QUIC), ALPN and counts. It is the basis of most current TLS-fingerprint detection.
Does a residential proxy change my TLS fingerprint?
No. The proxy tunnels your handshake unchanged; the destination sees the proxy's IP and your ClientHello. Only the client software determines the fingerprint.
Can I fake a browser's TLS fingerprint in Python?
Yes, with impersonating clients such as curl_cffi, which reproduce a chosen browser's ClientHello and HTTP/2 behaviour. You must keep headers consistent with the same browser. Plain requests/httpx cannot do this.
Is TLS fingerprinting the same as browser fingerprinting?
No. TLS fingerprinting reads the encrypted-connection handshake and identifies the client software; browser fingerprinting runs JavaScript and identifies the device (canvas, fonts, GPU). Anti-bot systems use both, plus IP reputation, and check that they agree.
Why does the same script get blocked on one site and not another?
Because TLS fingerprint checks are a feature of enterprise anti-bot products, not of the web in general. Sites without such protection never look at the handshake; sites with it compare it to the User-Agent on every request.