All posts

TLS fingerprinting: check your own JA3 and JA4

What is TLS fingerprinting? It identifies a client by its ClientHello, hashed as JA3 or JA4. Learn to inspect yours and pick a client matching your User-Agent.

  • tls fingerprinting
  • ja3
  • ja4
  • clienthello
  • bot detection
On this page

TLS fingerprinting identifies a client from the ClientHello it sends, summarized as a JA3 or JA4 hash. Your TLS library writes that message, not any header you set, so a mismatch with your claimed User-Agent is the signal. Pick a client whose real stack matches the identity you claim instead of randomizing values.

The same URL returns a full page in your browser tab and a block page from your script. The User-Agent string is identical, and a header diff shows nothing. The likely cause is the ClientHello your HTTP library sent, which arrives before any header.

After reading, you’ll be able to decode a JA3 or JA4 value, inspect your own client’s hello, and choose a client class before tuning anything.

Key Takeaways

  • Per Fingerprint.com, JA3 hashes extensions in the order sent, so randomized order breaks it; JA4 sorts before hashing.
  • A client that shuffles ciphers matches no real browser, so a claimed-vs-observed check flags it.
  • TLS is one layer: TCP/IP, HTTP/2, headers, exit IP and JavaScript values must agree with it.
  • Hybrid post-quantum groups and ECH are changing the ClientHello, so test your own with browserleaks.com/tls.

What is TLS fingerprinting and where does the fingerprint come from?

TLS fingerprinting identifies a client by the parameters in its TLS ClientHello message: version, cipher suites, extensions, supported groups and ALPN values. The TLS library linked into the client chooses those values, so a server can tell Chrome, Firefox, curl or a script apart before any HTTP is sent.

Image generated

RFC 8446 says “When a client first connects to a server, it is REQUIRED to send the ClientHello as its first TLS message.” The same RFC notes that “All handshake messages after the ServerHello are now encrypted,” so the ClientHello itself travels in cleartext.

SNI is defined in RFC 6066. ALPN is defined in RFC 7301. Your application doesn’t write any of those bytes.

The library does, and its output describes the library and its configuration, not a person.

You can check this on one machine. Open browserleaks.com/tls in Chrome, then in Firefox, and compare what the two browsers show.

Chrome links BoringSSL, which its README calls “a fork of OpenSSL that is designed to meet Google’s needs.” Firefox uses NSS, which Mozilla describes as “a set of libraries designed to support cross-platform development of security-enabled client and server applications.”

This is why a mismatch is the signal. If your User-Agent says Chrome but the ClientHello comes from an HTTP client linked against OpenSSL, the two layers disagree.

Shuffling the cipher list in that client doesn’t fix it. The shuffled order matches no browser library’s output, so you’ve traded a known script fingerprint for one nobody real sends.

How are JA3 and JA4 TLS fingerprints computed?

JA3: five fields, one MD5

  1. Read five fields from the ClientHello: TLS version, cipher suites, extensions, supported groups (elliptic curves) and EC point formats. The JA3 README lists the order as SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat.
  2. Drop GREASE values first. GREASE, per RFC 8701, is “a mechanism to prevent extensibility failures in the TLS ecosystem”. The JA3 README says “JA3 ignores these values completely.”
  3. Write each value in decimal, in the order the client sent it, without sorting. The README says the method “concatenates those values together in order, using a ”,” to delimit each field and a ”-” to delimit each value in each field.”
  4. Hash the string with MD5, which the README says produces “an easily consumable and shareable 32 character fingerprint.”
import hashlib

def is_grease(v: int) -> bool:
    # RFC 8701 reserved values: 0x0a0a, 0x1a1a, ... 0xfafa
    return (v & 0x0F0F) == 0x0A0A and (v >> 8) == (v & 0xFF)

def ja3(version, ciphers, extensions, groups, point_formats) -> str:
    def join(vals):
        return "-".join(str(v) for v in vals if not is_grease(v))
    fields = [str(version), join(ciphers), join(extensions),
              join(groups), join(point_formats)]
    return hashlib.md5(",".join(fields).encode()).hexdigest()

Because step 3 keeps send order, a client that shuffles extensions produces a different hash on each connection.

JA4: a readable prefix and two sorted hashes

JA4 has three parts joined by underscores, and the FoxIO JA4 technical details define the layout.

  • Prefix (a): protocol (t for TCP, q for QUIC), two-digit TLS version, d or i for SNI present or absent, two-digit cipher count, two-digit extension count, and the first ALPN value (h2 for HTTP/2).
  • Part b: a truncated SHA-256 of the cipher list, with ciphers “sorted in hex order”.
  • Part c: a truncated SHA-256 of the sorted extensions, followed by the signature algorithms in the order sent. SNI and ALPN are counted in the prefix but left out of this hash.

GREASE is ignored here too. Sorting means shuffled extension order no longer changes parts b and c.

JA3JA4
OrderingAs sentCiphers and extensions sorted
Readable portionNonePrefix: protocol, version, SNI, counts, ALPN
HashMD5, 32 charactersTruncated SHA-256 sections
GREASERemovedIgnored

To check your own client against the fields above, read its JA3 and JA4 on BrowserLeaks TLS.

Why do JA3 hashes change when Chrome randomizes extension order?

JA3 hashes change under Chrome’s extension shuffling because JA3 hashes the extensions in the order the client sent them, so every new order gives a new MD5.

Chromium’s TLS library documents the switch. BoringSSL’s header says its permute option “configures whether sockets on |ssl| should permute extensions. For now, this is only implemented for the ClientHello.”

GREASE adds a second moving part, since its reserved values vary between connections. Both computations drop those values, so the shuffle, not GREASE, is what breaks JA3.

JA4 sidesteps the shuffle by sorting. Its technical details describe the extension section as a “truncated sha256 hash of the list of extensions, sorted in hex order.” Any permutation of the same extensions lands on the same value.

To see this, open browserleaks.com/tls in Chrome, note the JA3 and the extension list, then reload and compare. The BrowserLeaks page, per its own description, shows “cipher suites, extensions, and key exchange groups” and generates the fingerprint “in JA3/JA4 formats.”

Expect the JA4 to hold steady while the JA3 changes. Three consequences follow:

  • A table that lists one JA3 per browser version is a snapshot of single captures, not a stable identifier.
  • Match on JA4, or on field sets (ciphers, groups, ALPN, the extension set), and treat hash tables as hints.
  • Don’t build a client around copying one captured hash. You’d be reproducing one draw from a shuffled distribution, and that hash won’t stay representative of the browser.

Why does shuffling ciphers fail a TLS fingerprinting check?

A shuffled cipher list fails because a detector compares the ClientHello with the browser your User-Agent claims, and the shuffled order matches neither that browser nor your library.

The check needs no page script. As one explainer puts it, TLS fingerprinting “extracts that signature from the handshake to identify the client software making the request, without relying on JavaScript or cookies at all.”

What the detector expects depends on who writes the hello:

Client classWho writes the ClientHelloWhat the User-Agent must matchTypical failure
Real browserThe browser’s own TLS libraryIts own build, by constructionNone from TLS alone
Headless or automated browserThe same engine’s TLS libraryThe engine and mode it runs inHeadless mode differs from headed
TLS-impersonating HTTP clientA library that copies a browser’s helloThe one browser version it copiesCopy drifts as the browser changes
Stock HTTP libraryThe library’s linked TLS stackNothing, it isn’t a browserBrowser User-Agent on a non-browser hello

JA3 hashes the cipher list in the order sent, so every new order gives a new hash. JA4 sorts ciphers, so the shuffle changes less there.

The cipher set, extension set and ALPN values must still match a real browser, and a stock library’s set won’t.

Randomizing weakens hash matching, but it doesn’t make you look like a browser. Fingerprint.com notes that JA3 and JA4 reliability “drops when attackers randomize or frequently rotate configurations.” That describes the detector’s confidence, not your cover.

Decide the client class first. Then compare your own hello on BrowserLeaks with the User-Agent you send.

Each class fits a different job:

  • Real browser: sessions where the hello must match the claimed identity. Our anti-detect browser sessions use Chromium over CDP, Chromium only.
  • Headless or automated browser: testing where you control the engine.
  • TLS-impersonating HTTP client: high-volume fetches where you accept the drift upkeep.
  • Stock HTTP library: targets that don’t inspect TLS.

What other signals sit next to TLS fingerprinting?

TLS fingerprinting is one layer in a stack of at least five signals, and a detector can check each layer against the others. FoxIO’s JA4+ suite names several of them.

  1. TCP/IP (JA4T, JA4L). FoxIO’s repository lists JA4T as TCP client fingerprinting and JA4L as “Light Distance/Latency Fingerprinting”. Your operating system’s network stack sets these, not your script.
  2. TLS (JA4). The ClientHello your TLS library writes: versions, ciphers, extensions, ALPN. You can read your own at browserleaks.com/tls.
  3. HTTP/2 and headers (JA4H). JA4H is listed as “HTTP Client Fingerprinting”. It covers header order and values, so a Chrome User-Agent sent with a library’s header order is a mismatch.
  4. Exit IP. The address has to agree with the claimed locale and timezone. A US timezone behind a distant exit is a contradiction no header fixes.
  5. JavaScript-visible values. Screen size, window size and other browser properties must match the claimed OS and device.

Licensing matters if you plan to build on these. The JA4 repository states that “JA4: TLS Client Fingerprinting is open-source, BSD 3-Clause, same as JA3.” Its other JA4+ methods fall under the FoxIO License.

State has to agree too. Wayfern’s buyer’s guide puts it this way: “A fingerprint that passes the check on page one is worth nothing if the cookie jar, the local storage and the exit IP change by page four.”

We built our model around that. In Wayfern, the identity is applied by the browser build, and you drive the hosted browser over CDP, as the page on anti-detect browser sessions shows.

The honest catch: Wayfern is Chromium-only on Linux and presents a desktop device. You get no per-vector fingerprint switches and no TLS settings. If you need that control, use a desktop profile manager, or a TLS-impersonating HTTP client when you don’t need a browser.

What do X25519MLKEM768 and Encrypted Client Hello change?

Two changes to the ClientHello make older fingerprint write-ups stale: hybrid post-quantum key exchange adds new group values, and Encrypted Client Hello (ECH) wraps part of the message in encryption.

Hybrid groups change two fields

IANA’s TLS Supported Groups registry lists X25519MLKEM768, which is codepoint 0x11EC. A client that offers it adds that value to its supported-groups list.

It also sends a key share for it, so the key-share extension gets larger.

The group is defined in an IETF draft, draft-ietf-tls-ecdhe-mlkem. Which browser versions send it depends on the build, so read your own client’s output instead of a version list.

A hash table made before this group existed won’t contain the new values. That’s one more reason to treat old per-version hash lists as dated.

ECH splits the hello in two

The IETF TLS Working Group’s ECH specification says it “describes a mechanism in Transport Layer Security (TLS) for encrypting a ClientHello message under a server public key.”

The result is an outer ClientHello sent in the open, carrying an extension that holds the encrypted inner ClientHello. Check the datatracker page for the document’s current status, since it changes.

The encrypted part is hidden from a passive observer, but the outer hello is still a ClientHello. Its cipher suites, extensions and groups can still be read and hashed.

The spec doesn’t promise that TLS-level classification stops working, and ECH deployment depends on both the client and the server.

See it on your client

BrowserLeaks’ TLS test shows “supported TLS protocols, cipher suites, extensions, and key exchange groups.” Run it from your browser and from your HTTP client, then compare the groups lists and extension order side by side.

How do you inspect your own TLS fingerprint and pick the right client?

You inspect your own TLS fingerprint by requesting a public test page from the exact client you plan to run, then reading the result before you tune anything.

  1. Request the page from your client. From an HTTP client, call the JSON endpoint of browserleaks.com/tls and save the TLS Fingerprint, cipher and extension lists. Then open the same page in a real Chromium browser and save the same fields.
  2. Read the JA4 prefix. Per FoxIO’s JA4 docs, the first section encodes protocol, TLS version, SNI flag, cipher count, extension count and first ALPN value. Different counts between your two runs mean different ClientHello messages.
  3. Compare with the browser you claim. If your User-Agent says Chrome and your HTTP client’s lists differ from the Chromium run, a detector sees the same mismatch you do.

To run the check through a hosted browser, connect over CDP:

import { chromium } from "playwright";
// The ClientHello comes from the browser build, not from this script.
const browser = await chromium.connectOverCDP("wss://api.wayfern.com/ws?token=LINK_TOKEN");
const page = await browser.contexts()[0].newPage();
await page.goto("https://browserleaks.com/tls");

Choose the client class next, then tune:

  • Real Chromium, driven by code, one consistent identity: a hosted Chromium over CDP fits. Wayfern describes itself as “Hosted anti-detect Chromium.”
  • Pages only, no JavaScript: a TLS-impersonating HTTP client is lighter. Our web scraping tools guide compares page-based and session-based options.
  • A person hand-managing many long-lived identities: a desktop profile manager gives more control. See our anti-detect browser comparison.

Whichever class you pick, use it for public-data collection, testing and monitoring within site terms and the law.

Frequently asked questions

Can I change my TLS fingerprint?

You can, but aim for consistency, not randomness. Use a client whose TLS stack matches the browser you claim, such as a real Chromium build or a TLS-impersonating HTTP client. Keep User-Agent, HTTP/2 settings and exit IP consistent, since shuffled ciphers produce a ClientHello no real browser sends.

Is JA3 still useful now that JA4 exists?

JA4 is the current baseline. JA3 hashes fields in the order sent, so shuffled extensions change it, while JA4 sorts first. Fingerprint.com says both hashes lose reliability when configurations are randomized or rotated, so treat either as one signal.

Does a residential proxy change my TLS fingerprint?

No. A CONNECT tunnel carries your client’s own ClientHello end to end, because RFC 9110 defines it as blind forwarding of packets. The exit changes the IP signal, not the fingerprint. A proxy that inspects HTTPS terminates TLS and sends its own.

Can I set TLS values on Wayfern?

No. As the Wayfern vs Multilogin comparison states, customers get no per-vector fingerprint switches: the browser build applies the identity. If you want hand-tuned profiles, a desktop profile manager fits better.

What to do next

  • Today: Open browserleaks.com/tls from the exact client you plan to run. Read the JA4 prefix and note what it reports.
  • This week: Decide your client class: real browser, TLS-impersonating HTTP client, or profile manager. Then check that the User-Agent, HTTP/2 settings, locale and exit all agree with that choice.
  • Before shipping: Re-run the check after every browser or library upgrade. Extension order, post-quantum groups and ECH change ClientHello contents, so last month’s hash may not match today’s.

Then make the call. If you need real Chromium driven over CDP, start from the docs quickstart and check rates on the pricing page before you plan a run.

If you want per-vector control, a desktop profile manager fits better. If you only need pages, a TLS-impersonating HTTP client is enough for TLS fingerprinting purposes.

Sources

TLS Fingerprinting Explained: How JA3/JA4 Identify Your… · TLS Client Test - TLS Fingerprinting - BrowserLeaks · TLS Fingerprinting: What It Is + How It Works · RFC 8446: The Transport Layer Security (TLS) Protocol… · BoringSSL · salesforce/ja3: A method for profiling SSL/TLS Clients · JA4 technical details, FoxIO-LLC/ja4 · FoxIO-LLC/ja4: JA4+ Network Fingerprinting. Every other source is linked where the article cites it.

See us more often in Google

One click marks Wayfern as a preferred source in Google, so our posts can show up more often in your Top Stories, AI Mode and AI Overviews.