🎉 We just launched long anticipated residential proxies & pay-per-GB!
Pay once, switch between multiple proxy providers.
50% discount for a limited time. See more →

TLS Fingerprinting Through a Proxy: JA3 & JA4

published 2026-09-10
by Paul Wang
26 views

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.

TLS ClientHello fields feed JA3 and JA4 summaries; HTTP/2 fingerprinting measures behavior after TLS negotiation.
JA3 and JA4 summarize selected ClientHello fields. HTTP/2 fingerprinting measures a separate protocol layer.

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.

FingerprintWhat it summarizes and formatLimitation
JA3Selected ClientHello fields; 32-character MD5 hex digest.Extension-order randomization can change it between otherwise similar connections.
JA3SSelected ServerHello fields; 32-character MD5 hex digest.It describes the server response, not the whole client.
JA4TLS version, SNI class, ALPN, cipher-suite and extension hashes; t..._..._....Equal values summarize compatible shapes; they do not make two handshakes byte-identical.
JA4HAn HTTP request summary; underscore-separated JA4H fields.It is separate from TLS and depends on the request.
Akamai HTTP/2HTTP/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

Opaque tunnels pass the client handshake through; interception creates two TLS connections; a cache hit sends no request to the origin.
Opaque tunneling, intercepted misses or bypasses, and local cache hits expose different information to the origin.
Proxy typeTLS and HTTP/2 at originSource network identityOther signals
SOCKS5 or opaque CONNECTThe 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 MITMThe 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 proxyA 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 proxyMay imitate a chosen TLS or HTTP/2 profile.Changes to the proxy exit.It still needs coherent headers, timing, and application behavior.
VPNNormally 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 familyConfigurations tested
curl7.74.0/OpenSSL 1.1.1w and 8.22.0/OpenSSL 4.0.2, each on HTTP/1.1 and HTTP/2
Python3.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 GoNode 24.20.0/OpenSSL 3.5.7 and Go 1.27.1 net/http, each on HTTP/1.1 and HTTP/2
BrowserChromium reporting HeadlessChrome/149.0.7827.0, run through Playwright build 1228
Measure on preserving pathsObserved result
Clients with one JA417 of 17
HTTP/2 clients with one Akamai HTTP/2 hash10 of 10
HTTP/1.1 clients7; HTTP/2 hash not applicable
Successful observations per client15, 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.

InterceptorDistinct JA4 valuesDistinct HTTP/2 hashesObserved protocol behavior
Stock Go control1 across 17 clients1Made HTTP/1.1 clients arrive over HTTP/2 because ForceAttemptHTTP2 was enabled.
mitmproxy 9.0.13, distinguished by ALPN1Kept 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.

CountEndpoint and pathConfigurations
10BrowserLeaks through mitmproxyAll 10 HTTP/2 configurations
3BrowserLeaks, direct, local opaque, and eu-1 opaquePython httpx HTTP/2
1tls.peet.ws, eu-1 opaquecurl 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

Compare JA4, repeated JA3 values and applicable HTTP/2 hashes; expect randoms, key shares, session identifiers, exit IP and timing to differ.
Run separate direct, opaque-proxy and cache-enabled comparisons. Matching summaries do not establish identical connections.

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.

  1. Record a direct baseline.
  2. Repeat through an opaque proxy without changing the client or HTTP preference.
  3. For Litport cache mode, change only the cache suffix and required CA trust.
  4. 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.

EndpointCompareDo not treat as stable
tls.peet.wstls.ja3_hash, tls.ja4, http2.akamai_fingerprint_hashClient random, session identifiers, network address, TCP values, and timing
BrowserLeaksja3_hash, ja4, akamai_hashFormatting 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 configurationJA3JA4Akamai HTTP/2
curl 7.74.0 HTTP/1.1f436b9416f37d134cadd04886327d3e8t13d3113h1_e8f1e7e78f70_ce5650b735ce
curl 7.74.0 HTTP/2f436b9416f37d134cadd04886327d3e8t13d3113h2_e8f1e7e78f70_ce5650b735cefd07c74c4990fc43ef9f4b3277cf3cf2
curl 8.22.0 HTTP/1.19ff5f7c88ab8372852860f487a8b1dc5t13d3012h1_1d37bd780c83_8deb338c2fb1
curl 8.22.0 HTTP/29ff5f7c88ab8372852860f487a8b1dc5t13d3012h2_1d37bd780c83_8deb338c2fb155cb2bd667724e6d46cc3c8dd15d50de
Python urllib HTTP/1.1c199b43d41b470f8f68c5561f8f1ce3et13d3111_e8f1e7e78f70_d41ae481755e
Python requests HTTP/1.1bc29aa426fc99c0be1b9be941869f88at13d3112h1_e8f1e7e78f70_b26ce05bbdd6
Python httpx HTTP/1.1657a105ca838d7171613c035060a5fedt13d3112h1_e8f1e7e78f70_d41ae481755e
Python httpx HTTP/2657a105ca838d7171613c035060a5fedt13d3112h1_e8f1e7e78f70_d41ae481755e29e6a0e9b360185223ec1278a845fdb1
curl_cffi, no profilec3c1610cb4fbc856ed4f0652eddd3288t13d2812h2_257f3020b3a2_cbb9361b6bf952d84b11737d980aef856699f885ca86
curl_cffi Chrome8f622479e14eae70627f592c7d08c18ct13d1516h2_8daaf6152771_d8a2da3f94cd52d84b11737d980aef856699f885ca86
curl_cffi Firefox6f7889b9fb1a62a9577e685c1fcfa919t13d1717h2_5b57614c22b0_3cbfd9057e0d6ea73faa8fc5aac76bded7bd238f6433
curl_cffi Safari773906b0efdefa24a7f2b8eb6985bf37t13d2014h2_a09f3c656075_e42f34c56612c52879e43202aeb92740be6e8c86ea96
Node 24.20.0 HTTP/1.1d67b094811e5145139d7cea5f014309ft13d5212h1_b262b3658495_8e6e362c5eac
Node 24.20.0 HTTP/2d67b094811e5145139d7cea5f014309ft13d5212h2_b262b3658495_8e6e362c5eac38e36840395e07ae27bae1632ddc8b92
Go 1.27.1 HTTP/1.19b7dcdf3f997f1fb7b4409c94cb7ef36t13d1311_f57a46bbacb6_f50d94e863eb
Go 1.27.1 HTTP/203117a8ed39ef02427ebbc39f121275ct13d1312h2_f57a46bbacb6_f50d94e863ebb4e6bd27e907d4aa4316619ce615fda4
HeadlessChrome/149.0.7827.0d08080b84c354b95eeacbaf91fb44e09t13d1516h2_8daaf6152771_d8a2da3f94cd52d84b11737d980aef856699f885ca86

Sources

Paul Wang
Paul is an experienced web-developer with more 15 years of experience working with proxy servers and scrapers. He helps us on litport.net with software engineering and quality assurance.