TLS fingerprinting identifies your client from the structure of its ClientHello message: the cipher list, extensions, curves and version order. It happens before any HTTP data is exchanged, cannot be changed from browser settings, and is why a spoofed User-Agent on curl still gets blocked.
What is being fingerprinted
Every TLS connection opens with a ClientHello, the first message your client sends. It is unencrypted by necessity, because the two sides have not yet agreed on how to encrypt anything. It declares the highest TLS version supported, a list of cipher suites in preference order, a list of extensions, the elliptic curves supported, the point formats, the ALPN protocols offered, and the server name being requested.
None of that identifies you personally. All of it identifies your software with considerable precision. Chrome 141 on macOS offers a specific set of ciphers in a specific order with a specific extension list. OpenSSL's command line offers a different set. Python's requests library, Go's net/http, curl, Node.js and every automation framework each have a distinctive signature.
The observation that makes this powerful is that the signature is a property of the TLS library and its configuration, not of anything the user or the application layer controls. You cannot change it from browser settings, an extension, or an HTTP header, because it is emitted below the layer any of those operate at.
You can look at your own. The -msg flag prints the raw handshake, and curl -v shows the negotiated result.
openssl s_client -connect example.com:443 -msg -tls1_3 </dev/null 2>&1 | head -n 60
curl -v --tlsv1.2 https://example.com 2>&1 | grep -E "SSL connection|ALPN|cipher"
# capture a ClientHello for inspection
sudo tcpdump -i any -s0 -w hello.pcap 'tcp port 443'
# then in Wireshark: tls.handshake.type == 1JA3 and what replaced it
JA3, published by Salesforce engineers in 2017, was the first widely adopted method. It concatenates five fields from the ClientHello into a string, then takes the MD5 hash of it. The fields are: TLS version, the accepted ciphers, the list of extensions, the elliptic curves, and the elliptic curve point formats, each as comma-separated decimal values, the groups joined by commas.
The result is a 32-character hexadecimal value that can be matched against a database of known clients. JA3S is the server-side equivalent, hashing the ServerHello, and the pair together identifies a client-server combination, which is how malware command-and-control traffic was originally detected with it.
JA3 has a serious weakness that emerged after Chrome began randomising its extension order in 2023. Because extension order is part of the hashed input, a browser that shuffles it produces a different JA3 hash on every connection, making the value useless for identification.
JA4, part of the JA4+ family introduced by FoxIO, was designed around that problem. Instead of one opaque hash, JA4 produces a human-readable structured string: a protocol and version prefix, whether SNI is present, counts of ciphers and extensions, the ALPN value, and then truncated hashes of the sorted cipher and extension lists. Sorting the lists before hashing makes the value stable under randomisation, and the readable prefix means an analyst can interpret parts of it without a lookup table.
The wider JA4+ suite extends the same idea to other layers: JA4S for the server hello, JA4H for HTTP headers, JA4X for X.509 certificates, JA4L for latency and locality, and JA4T for TCP characteristics.
# JA3 input format
# SSLVersion,Ciphers,Extensions,EllipticCurves,ECPointFormats
771,4865-4866-4867-49195-49199,0-23-65281-10-11-35-16-5-13,29-23-24,0
# -> MD5 -> cd08e31494f9531f560d64c695473da9
# JA4 output format, readable by design
# t13d1516h2_8daaf6152771_b0da82dd1658
# t = TCP
# 13 = TLS 1.3
# d = SNI present (i = no SNI)
# 15 = 15 cipher suites
# 16 = 16 extensions
# h2 = ALPN is HTTP/2Neither JA3 nor JA4 identifies an individual. They identify a client implementation and configuration. Their value to a defender is that a fingerprint contradicting the claimed User-Agent proves the client is not what it says it is, which is enough to act on.
Why curl gets a 403 and Chrome does not
This is the most common practical encounter with TLS fingerprinting. You copy a request from the browser's network tab as a curl command, including every header and cookie, run it, and receive a 403 or a challenge page. The same URL loads fine in the browser.
The reason is that you copied everything above the transport and nothing below it. curl links against OpenSSL, GnuTLS or Secure Transport depending on the build, and each produces a ClientHello structurally different from Chrome's BoringSSL. The edge sees a fingerprint matching a command-line tool and headers claiming to be Chrome, and treats the contradiction as sufficient evidence of automation.
Tooling exists to close the gap. curl-impersonate and the Python curl_cffi binding are curl builds patched to reproduce specific browsers' TLS and HTTP/2 signatures. In Go, the utls library allows a program to specify a ClientHello template matching a chosen browser. These are legitimate tools for interoperability testing and for accessing your own services; they do not change the ethics of what you point them at.
- A ClientHello that matches Chrome exactly is necessary but not sufficient; HTTP/2 settings must match too.
- Sending browser headers over a non-browser TLS stack is a stronger bot signal than sending honest headers.
- Header order is checked alongside the fingerprint, because real browsers emit headers in a fixed sequence.
- TLS session resumption behaviour and the presence of session tickets add further distinguishing detail.
The layers above: HTTP/2 and beyond
TLS is only the first layer of transport fingerprinting. Once the connection is established, HTTP/2 provides another distinctive signature, commonly called the Akamai HTTP/2 fingerprint.
It is derived from the SETTINGS frame the client sends at connection start, specifically the parameter identifiers and values and the order in which they appear; the WINDOW_UPDATE increment; whether priority frames are sent and what they contain; and the pseudo-header order in the first request, since the sequence of :method, :authority, :scheme and :path differs consistently between implementations.
Chrome, Firefox and Safari each produce recognisably different HTTP/2 fingerprints, and they differ again from Go, Python's httpx and Node.js. Matching a browser's TLS fingerprint while sending a Go HTTP/2 profile is a mismatch that detection systems check for specifically.
Below TLS, TCP and IP fields provide a coarser signal: initial window size, TTL as received, and TCP options and their order. This is classic passive OS fingerprinting, still implemented by tools such as p0f, and it is most useful to a defender as a contradiction check. QUIC and HTTP/3 have their own equivalents, since the QUIC initial packet exposes transport parameters directly.
Randomisation, GREASE and ECH
Three mechanisms complicate fingerprinting, none of which was designed to defeat it.
GREASE, specified in RFC 8701, has clients insert deliberately invalid values into cipher and extension lists. Its purpose is to stop middleboxes ossifying around the exact values in use today, by ensuring any implementation that cannot tolerate unknown values fails immediately rather than years later. Fingerprinting methods must strip GREASE values before hashing, which both JA3 and JA4 do.
Chrome's extension order randomisation, introduced in 2023, shuffles the extension list on each connection for the same anti-ossification reason. Its side effect was breaking JA3 for Chrome traffic, which accelerated adoption of JA4's sorted-hash approach.
Encrypted Client Hello encrypts the sensitive parts of the ClientHello, most importantly the server name indication, using a public key published in the DNS record for the destination. It removes SNI from plaintext observation, which matters for network-level censorship and for ISP visibility into which sites you visit. It does not hide the outer ClientHello structure, so the fingerprint of your client remains visible to any observer, and entirely visible to the server. ECH protects what you are connecting to, not what you are connecting with.
For ECH to function, the client must be able to fetch the HTTPS resource record from DNS, which in practice requires encrypted DNS. Cloudflare-fronted sites publish the necessary records; adoption elsewhere is uneven.
# check whether a site publishes ECH configuration
dig +short HTTPS crypto.cloudflare.com
dig +short HTTPS cloudflare.com | grep -o "ech=[A-Za-z0-9+/=]*"
# Firefox: about:config
network.dns.echconfig.enabled = true
network.dns.http3_echconfig.enabled = trueECH only helps when the DNS lookup that retrieves the ECH key is itself protected. Using ECH with plaintext DNS to your ISP's resolver leaves the destination visible in the query, which defeats the purpose.
What you can realistically do
For an ordinary user, the honest answer is very little, and that is mostly fine. TLS fingerprinting identifies your browser and platform, not you. Millions of people share the exact fingerprint of a current Chrome build on Windows, which makes it a poor individual identifier on its own. It becomes a privacy problem only when combined with other signals, and in that combination it is rarely the weakest link.
Use a mainstream browser at a current version. This is the entire practical recommendation. A common fingerprint is an anonymous one. Unusual browsers, old versions, and anything with a modified TLS stack all produce rarer signatures.
Do not install extensions that claim to spoof or randomise your TLS fingerprint. They cannot reach that layer from within the browser's extension sandbox, and any observable effect they do produce makes you more distinctive rather than less.
Enable encrypted DNS and ECH where available. These address a different and more consequential exposure, namely which sites you visit being visible to your network, rather than which client you are using.
For defenders, treat fingerprints as one input. A JA4 value matching a known automation library is evidence, not proof, and rate limits or challenges are proportionate responses where an outright block is not.
Frequently asked
Can I change my TLS fingerprint?
Not from browser settings, extensions or headers, because the fingerprint is produced by the TLS library below all of those. It changes when you change browser or browser version, or when a developer explicitly uses a library such as utls or curl-impersonate that constructs a specific ClientHello. For ordinary browsing there is no user-facing control.
Does a VPN hide my TLS fingerprint?
No. A VPN changes the source IP address of your traffic; the ClientHello you send to the destination server is unchanged and fully visible to it. VPN and fingerprint are orthogonal, which is why a service can see a residential exit address and still classify the connection as automation.
What is the difference between JA3 and JA4?
JA3 is a single MD5 hash over ordered ClientHello fields, which breaks when a browser randomises extension order. JA4 produces a readable structured string with sorted, truncated hashes, so it stays stable under randomisation and is partially interpretable without a lookup database. JA4 is part of a broader JA4+ suite covering other protocol layers.
Why does my script get blocked when my browser does not?
Your script's TLS fingerprint identifies the library it uses, and it contradicts the browser headers you copied. Detection systems flag that mismatch specifically. Matching the headers is not enough; the ClientHello structure and HTTP/2 settings would have to match too, which requires a purpose-built client.
Does Encrypted Client Hello stop TLS fingerprinting?
No. ECH encrypts the inner ClientHello including the server name, hiding which site you are visiting from network observers. The outer ClientHello, which carries the cipher and extension structure that fingerprinting uses, remains in plaintext, and the destination server sees everything regardless. ECH protects the destination, not the client identity.