TLS Fingerprinting Through a Proxy: JA3 & JA4
A proxy can give your requests a different IP address while leaving your client’s TLS fingerprint intact. Websites can recognize patterns in the handshake that establishes the encrypted connection, before they receive an HTTP request.
An opaque SOCKS5 or HTTP CONNECT tunnel forwards that handshake. TLS interception creates a new outbound handshake, which can reveal the proxy’s transport stack instead.
We tested 17 client configurations across seven connection paths to see which JA3, JA4, and HTTP/2 signals changed. This article explains the results and shows how to check your own client.
What TLS fingerprinting measures
TLS starts before HTTP. In TLS 1.3, the client sends a ClientHello that proposes protocol versions, cipher suites, extensions, supported groups, signature algorithms, ALPN protocols, and other capabilities. Much of that offer is visible to the server before application data is encrypted. RFC 8446 defines the handshake; it does not define a universal fingerprint.
A fingerprint is a compact summary of selected fields, not a person identifier or an authorization decision. It can identify a connection implementation or profile, and it can change when a library or browser changes. Browser fingerprinting adds different signals from the page itself; see our browser fingerprinting guide.
JA3, JA3S, JA4, JA4H, and the HTTP/2 hash
These summaries answer different questions. JA3 and JA4 describe the ClientHello, JA3S describes the server response, JA4H summarizes an HTTP request, and an HTTP/2 fingerprint observes behavior after TLS negotiation. An HTTP/1.1 connection has no HTTP/2 fingerprint to compare.
| Fingerprint | What it summarizes and format | Limitation |
|---|---|---|
| JA3 | Selected ClientHello fields; 32-character MD5 hex digest. | Extension-order randomization can change it between otherwise similar connections. |
| JA3S | Selected ServerHello fields; 32-character MD5 hex digest. | It describes the server response, not the whole client. |
| JA4 | TLS version, SNI class, ALPN, cipher-suite and extension hashes; t..._..._.... | Equal values summarize compatible shapes; they do not make two handshakes byte-identical. |
| JA4H | An HTTP request summary; underscore-separated JA4H fields. | It is separate from TLS and depends on the request. |
| Akamai HTTP/2 | HTTP/2 SETTINGS and frame behavior; 32-character hexadecimal digest. | HTTP/1.1 clients do not emit an HTTP/2 fingerprint. |
Cloudflare’s JA3/JA4 documentation explains why JA4’s handling of extensions is useful. Here, Chromium and curl_cffi’s Chrome profile produced 15 distinct JA3 values over 15 preserving-path observations, while each held one JA4. curl 8.22.0 HTTP/2 and Go 1.27.1 HTTP/2 each held one JA3 and one JA4 over 15 observations.
What a proxy can change
| Proxy type | TLS and HTTP/2 at origin | Source network identity | Other signals |
|---|---|---|---|
| SOCKS5 or opaque CONNECT | The client sends its own handshake and HTTP/2 frames through the tunnel. | Usually changes to the proxy exit. | The exit’s TCP/IP behavior and network reputation can differ. |
| TLS-terminating MITM | The proxy makes a separate outbound TLS connection, so its library and HTTP stack are visible. | Changes to the proxy exit. | It can also rewrite headers or HTTP version. |
| Caching proxy | A cache that terminates TLS can substitute its own transport stack unless it deliberately preserves compatible shape on misses and bypasses. | Changes to the proxy exit. | Cached responses have separate application-level behavior. |
| TLS-spoofing proxy | May imitate a chosen TLS or HTTP/2 profile. | Changes to the proxy exit. | It still needs coherent headers, timing, and application behavior. |
| VPN | Normally carries the existing TLS connection without terminating it. | Changes to the VPN exit. | TCP/IP and routing properties can change. |
Why interception cannot be byte-identical
A TLS-terminating proxy finishes one TLS connection and starts another. It can preserve selected ClientHello shape, but it cannot forward the original encrypted connection while also decrypting responses for caching or inspection.
- TLS material: a full handshake has a new client random and ephemeral key share. A resumed connection can use a PSK or ticket selected for that connection.
- Network path: the exit’s TCP stack, path MTU, loss, and timing can differ.
- HTTP/2 state: a new connection has its own SETTINGS exchange and dynamic HPACK table.
- Protocol constraints: ECH, 0-RTT, certificate pinning, and HTTP/3 add further limits.
Equal JA4 and Akamai values show that selected summaries matched. They do not prove byte-level identity or make a client undetectable.
The measurement
Measured September 9, 2026, the Litport-run study used 17 client configurations across seven paths and two diagnostic services: tls.peet.ws and BrowserLeaks. Each client/path combination made three tls.peet.ws attempts and one BrowserLeaks attempt: 476 attempts, 462 successes, and 14 failures.
- Preserving paths: direct; local Litport opaque CONNECT and cache mode; live eu-1 Litport opaque CONNECT and cache mode.
- Intercepting paths: a stock Go crypto/tls control with
ForceAttemptHTTP2: true, and mitmproxy 9.0.1 with default settings.
| Client family | Configurations tested |
|---|---|
| curl | 7.74.0/OpenSSL 1.1.1w and 8.22.0/OpenSSL 4.0.2, each on HTTP/1.1 and HTTP/2 |
| Python | 3.9.2 urllib; requests 2.32.5; httpx 0.28.1 on HTTP/1.1 and HTTP/2; curl_cffi 0.13.0 with no profile plus Chrome, Firefox, and Safari profiles |
| Node and Go | Node 24.20.0/OpenSSL 3.5.7 and Go 1.27.1 net/http, each on HTTP/1.1 and HTTP/2 |
| Browser | Chromium reporting HeadlessChrome/149.0.7827.0, run through Playwright build 1228 |
| Measure on preserving paths | Observed result |
|---|---|
| Clients with one JA4 | 17 of 17 |
| HTTP/2 clients with one Akamai HTTP/2 hash | 10 of 10 |
| HTTP/1.1 clients | 7; HTTP/2 hash not applicable |
| Successful observations per client | 15, except curl 8.22.0 HTTP/1.1, which had 14 |
The seven HTTP/1.1 configurations were curl 7.74.0, curl 8.22.0, Python urllib, Python requests, Python httpx, Node, and Go. Their absent HTTP/2 hashes are not passes. The counts use successful tls.peet.ws observations, which expose the TLS and HTTP/2 fields needed here.
| Interceptor | Distinct JA4 values | Distinct HTTP/2 hashes | Observed protocol behavior |
|---|---|---|---|
| Stock Go control | 1 across 17 clients | 1 | Made HTTP/1.1 clients arrive over HTTP/2 because ForceAttemptHTTP2 was enabled. |
| mitmproxy 9.0.1 | 3, distinguished by ALPN | 1 | Kept the clients’ observed HTTP versions in this run. |
These are mechanism demonstrations of those implementations, not a proxy-product benchmark.
curl_cffi’s Chrome profile and Chromium had equal JA4 and Akamai HTTP/2 hashes on each preserving path. That is a useful compatibility observation for those exact versions, not a promise that the two clients are indistinguishable to every service. The Python results make a related point: urllib, requests, and httpx ran on one interpreter yet emitted three JA4 values. Client behavior matters as much as the underlying TLS library.
Failures and limits
There were 476 attempts, 462 successes, and 14 failures. The failures were concentrated at BrowserLeaks rather than tls.peet.ws.
| Count | Endpoint and path | Configurations |
|---|---|---|
| 10 | BrowserLeaks through mitmproxy | All 10 HTTP/2 configurations |
| 3 | BrowserLeaks, direct, local opaque, and eu-1 opaque | Python httpx HTTP/2 |
| 1 | tls.peet.ws, eu-1 opaque | curl 8.22.0 HTTP/1.1 |
Direct and proxied observations were separate connections. Equal JA4 values cannot show that one randomized extension order was reproduced, because JA4 normalizes that order. They show equal selected summaries, not byte-level identity, a cache hit, an exit-address count, or a general proxy benchmark.
How detectors combine mismatches
Defenders rarely need one perfect hash. They can compare a User-Agent claim to the TLS library, header order and casing, ALPN, HTTP/2 SETTINGS and priority behavior, Client Hints, cookies, account history, request rate, and network context. A mismatch can be a useful review signal, but a single mismatch is not proof of abuse. The same principle appears in our anti-bot protection guide: decisions should combine context and leave a recovery path for legitimate users.
For HTTP/2 specifically, the original Akamai paper and lwthiker’s HTTP/2 analysis are useful starting points. These details explain why replacing a User-Agent string alone does not reproduce a browser’s connection behavior. This measurement did not run a User-Agent substitution experiment, so it makes no claim about one.
Check your own TLS fingerprint
Use a diagnostic endpoint only with traffic you are allowed to send. tls.peet.ws and BrowserLeaks are third-party services, so do not send credentials, customer identifiers, or sensitive request data.
- Record a direct baseline.
- Repeat through an opaque proxy without changing the client or HTTP preference.
- For Litport cache mode, change only the cache suffix and required CA trust.
- Repeat the comparison at a second diagnostic endpoint.
Direct baseline
curl --noproxy '*' --http2 https://tls.peet.ws/api/all
Opaque proxy
curl --proxy 'http://user:[email protected]:1337' \
--http2 https://tls.peet.ws/api/all
Litport cache mode
curl --proxy 'http://user:[email protected]:1337' \
--cacert ./litport-cache-root.crt --http2 https://tls.peet.ws/api/all
Second endpoint
curl https://tls.browserleaks.com/json
Replace user:pass with your Litport credentials. The cache example adds _cache-images to the password. The first command bypasses environment-configured proxies. --http2 requests HTTP/2 support but does not guarantee negotiation; check the response. Keep certificate verification enabled and obtain the cache root through the certificate setup guide.
| Endpoint | Compare | Do not treat as stable |
|---|---|---|
| tls.peet.ws | tls.ja3_hash, tls.ja4, http2.akamai_fingerprint_hash | Client random, session identifiers, network address, TCP values, and timing |
| BrowserLeaks | ja3_hash, ja4, akamai_hash | Formatting of an empty JA4 ALPN field can differ |
Repeat successful requests after a client, library, or proxy upgrade. In this study, tls.peet.ws left an empty JA4 ALPN field blank while BrowserLeaks used 00; the cipher and extension hashes matched. HTTP/1.1 legitimately has no HTTP/2 hash. For curl TLS options, see our curl SSL configuration guide.
Client notes
curl
The two curl builds produced different JA4 values. Treat --http2 as a separate configuration because HTTP/2 adds behavior for an origin to observe.
Python
urllib, requests, and httpx ran on one interpreter but emitted three JA4 values. httpx on HTTP/2 is another distinct configuration.
Node and Go
Node and Go were tested on HTTP/1.1 and HTTP/2. Compare the protocol you actually use. Go TLS defaults can also change with compatibility settings tied to a module’s go directive; see Go’s GODEBUG documentation.
Browser profiles
curl_cffi’s Chrome profile and Chromium had equal JA4 and Akamai HTTP/2 hashes on each preserving path. The Chrome, Firefox, and Safari names in curl_cffi are profiles, not browser installations. See curl_cffi, uTLS, and curl-impersonate for their documented interfaces.
Where Litport fits
Litport leaves ordinary CONNECT traffic opaque, so the target receives the client’s own handshake. Cache mode terminates TLS to serve cached responses and, on misses and bypasses, aims for compatible transport fingerprint-shape preservation rather than inserting a generic Go profile.
Cache interception requires the client to trust the Litport cache root. The results cover the clients tested and selected TLS and HTTP/2 summaries; they do not establish support for every TLS extension, browser release, or byte-level identity.
Read the feature details in transport fingerprint preservation, the proxy-cache overview, and the cache certificate guide. Certificate-pinned applications, real ECH, and HTTP/3 are outside this feature. Teams that need rotating network locations can review Litport residential proxies separately from their client implementation.
Frequently asked questions
What is a JA3 fingerprint?
JA3 is an MD5 digest of selected ClientHello fields. It is a convenient TLS client summary, but extension-order randomization can make it change between connections.
JA3 vs. JA4: which should I compare?
Compare both when a service exposes both. JA4 normalizes parts of the handshake that make JA3 volatile, while JA3 can still be useful for a particular client and observation method.
Does a proxy change my TLS fingerprint?
An opaque tunnel normally does not; a TLS-terminating proxy normally does. Test the exact client, proxy mode, and target because configuration determines the visible result.
Does a VPN change my TLS fingerprint?
A conventional VPN carries your TLS connection and does not terminate it, so the TLS summary should remain your client’s. The destination can still observe a different network path and exit address.
Does mitmproxy change a fingerprint?
Yes, in this test mitmproxy 9.0.1 made the outbound TLS connection and yielded three JA4 values across the client set. Its behavior should not be generalized to every version or configuration.
Can a proxy keep my HTTP/2 fingerprint?
An opaque proxy can transport it unchanged. A TLS-terminating proxy must deliberately reproduce compatible HTTP/2 behavior, and a matching hash remains a selected summary rather than a proof of identical connections.
How do I check my JA4?
Request a TLS diagnostic service repeatedly, compare the displayed JA4 values, then repeat through the proxy. Do not send private data to a public checker.
Does Cloudflare use JA4?
Cloudflare documents JA3 and JA4 fingerprint fields in its bot-management configuration. Whether a particular site uses a given signal is an operational choice of that site.
Why can a request be blocked even with a residential proxy?
Address reputation is one signal among many. Transport behavior, request headers, cookies, rate, account context, and site policy can all matter; work within the target’s permissions and access rules.
Is TLS fingerprinting the same as browser fingerprinting?
No. TLS fingerprinting observes the network handshake, while browser fingerprinting may include rendering, APIs, storage, and device characteristics. Each has different scope and limitations.
Full hashes from one direct tls.peet.ws observation per client configuration
These are measured direct observations. JA3 is the first direct observation shown; randomizing clients can emit a different JA3 on another connection. A dash means the client used HTTP/1.1 and had no Akamai HTTP/2 hash.
For the intercepting comparison, the stock Go control used t13d1312h2_f57a46bbacb6_a089bac06eae and b4e6bd27e907d4aa4316619ce615fda4. mitmproxy’s shared cipher and extension portions were c36778b98fa1_1f22a2ca17c4.
| Client configuration | JA3 | JA4 | Akamai HTTP/2 |
|---|---|---|---|
| curl 7.74.0 HTTP/1.1 | f436b9416f37d134cadd04886327d3e8 | t13d3113h1_e8f1e7e78f70_ce5650b735ce | — |
| curl 7.74.0 HTTP/2 | f436b9416f37d134cadd04886327d3e8 | t13d3113h2_e8f1e7e78f70_ce5650b735ce | fd07c74c4990fc43ef9f4b3277cf3cf2 |
| curl 8.22.0 HTTP/1.1 | 9ff5f7c88ab8372852860f487a8b1dc5 | t13d3012h1_1d37bd780c83_8deb338c2fb1 | — |
| curl 8.22.0 HTTP/2 | 9ff5f7c88ab8372852860f487a8b1dc5 | t13d3012h2_1d37bd780c83_8deb338c2fb1 | 55cb2bd667724e6d46cc3c8dd15d50de |
| Python urllib HTTP/1.1 | c199b43d41b470f8f68c5561f8f1ce3e | t13d3111_e8f1e7e78f70_d41ae481755e | — |
| Python requests HTTP/1.1 | bc29aa426fc99c0be1b9be941869f88a | t13d3112h1_e8f1e7e78f70_b26ce05bbdd6 | — |
| Python httpx HTTP/1.1 | 657a105ca838d7171613c035060a5fed | t13d3112h1_e8f1e7e78f70_d41ae481755e | — |
| Python httpx HTTP/2 | 657a105ca838d7171613c035060a5fed | t13d3112h1_e8f1e7e78f70_d41ae481755e | 29e6a0e9b360185223ec1278a845fdb1 |
| curl_cffi, no profile | c3c1610cb4fbc856ed4f0652eddd3288 | t13d2812h2_257f3020b3a2_cbb9361b6bf9 | 52d84b11737d980aef856699f885ca86 |
| curl_cffi Chrome | 8f622479e14eae70627f592c7d08c18c | t13d1516h2_8daaf6152771_d8a2da3f94cd | 52d84b11737d980aef856699f885ca86 |
| curl_cffi Firefox | 6f7889b9fb1a62a9577e685c1fcfa919 | t13d1717h2_5b57614c22b0_3cbfd9057e0d | 6ea73faa8fc5aac76bded7bd238f6433 |
| curl_cffi Safari | 773906b0efdefa24a7f2b8eb6985bf37 | t13d2014h2_a09f3c656075_e42f34c56612 | c52879e43202aeb92740be6e8c86ea96 |
| Node 24.20.0 HTTP/1.1 | d67b094811e5145139d7cea5f014309f | t13d5212h1_b262b3658495_8e6e362c5eac | — |
| Node 24.20.0 HTTP/2 | d67b094811e5145139d7cea5f014309f | t13d5212h2_b262b3658495_8e6e362c5eac | 38e36840395e07ae27bae1632ddc8b92 |
| Go 1.27.1 HTTP/1.1 | 9b7dcdf3f997f1fb7b4409c94cb7ef36 | t13d1311_f57a46bbacb6_f50d94e863eb | — |
| Go 1.27.1 HTTP/2 | 03117a8ed39ef02427ebbc39f121275c | t13d1312h2_f57a46bbacb6_f50d94e863eb | b4e6bd27e907d4aa4316619ce615fda4 |
| HeadlessChrome/149.0.7827.0 | d08080b84c354b95eeacbaf91fb44e09 | t13d1516h2_8daaf6152771_d8a2da3f94cd | 52d84b11737d980aef856699f885ca86 |