The ClientHello of every major browser, version by version
A modern Chrome ClientHello is roughly 1,700 bytes. A 2016 Chrome ClientHello was around 300. Most of that growth happened in two jumps, both driven by Google, and both leaving a mark that an edge proxy can read before a single byte of HTTP crosses the wire. If you want to know which browser, and often which major version, is sitting behind a TLS handshake, the ClientHello tells you most of it.
This is a catalog. The question it answers is narrow and practical: what has each major browser’s ClientHello looked like, version by version, from 2015 to now, and what does the current one look like in 2026? The interesting parts are the changes that moved the fingerprint, because those are the changes a passive observer keyed on. GREASE arriving in 2016. Extension permutation in early 2023 that quietly killed JA3. Post-quantum key shares in 2024 that blew the handshake past 1,500 bytes and gave servers a fresh, hard-to-fake tell.
The plan: first the anatomy of a ClientHello and why its fields are stable enough to fingerprint at all. Then a per-browser walk through Chrome, Firefox, Safari, and Edge, calling out the version where each shifted. Then the two structural changes that reshaped the whole catalog, GREASE and permutation, followed by the post-quantum transition that is the live story in 2026. The post closes on what a fresh fingerprint per browser looks like today, and why “looks like Chrome 138” is a moving target with a short shelf life.
What is in a ClientHello, and why it fingerprints
The ClientHello is the first message a TLS client sends. It is plaintext, even in TLS 1.3, because the handshake has not yet negotiated any keys. Inside it: the legacy version field, a 32-byte random, a session ID, the list of cipher suites the client supports in preference order, the compression methods, and then a block of extensions. The extensions carry the supported TLS versions, the named groups (elliptic curves and now post-quantum groups), the signature algorithms, ALPN, the key shares, SNI, and a long tail of optional features.
None of that is secret. All of it is chosen by the client’s TLS library, not by the user, and the choices differ between libraries in ways that are stable across millions of connections. Chrome ships BoringSSL. Firefox ships NSS. Safari uses Apple’s own stack. Each picks a particular cipher list, in a particular order, advertises a particular set of extensions, and lays out particular key shares. That combination is the fingerprint. The two best-known ways to summarize it are JA3, which hashes the version, cipher list, extension list, named groups, and EC point formats into an MD5, and the newer JA4, which we will get to.
The reason the ClientHello fingerprints so well is that the fields are deterministic per build and hard to change from outside the library. An HTTP client like Python’s requests cannot pick Chrome’s exact cipher order without reimplementing Chrome’s TLS stack, which is the whole subject of why your Python requests is fingerprintable before it sends a byte of HTTP. For the field-by-field anatomy that this catalog assumes you already half-know, see the TLS ClientHello, field by field.
Chrome: the browser that keeps moving the target
Chrome is the reference point, because Chrome is most of the web and because Google has changed its ClientHello more aggressively than anyone else. Three changes matter for the catalog: GREASE in 2016, two Google-specific extensions, and the post-quantum key share in 2024.
A Chrome 98 ClientHello on Windows 10 from early 2022 is a good snapshot of the pre-permutation era. It advertises 16 cipher suites, opening with the three TLS 1.3 suites (TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256) followed by ECDHE suites for both ECDSA and RSA, then legacy AES suites. The extension list, in the fixed order Chrome used at the time, ran through server_name, extended_master_secret, renegotiation_info, supported_groups, ec_point_formats, session_ticket, ALPN, status_request, signature_algorithms, signed_certificate_timestamp, key_share, psk_key_exchange_modes, supported_versions, and padding, with GREASE values inserted before and after the block. That handshake produced a stable JA3 of b32309a26951912be7dba376398abc3b for that build, which is exactly the kind of value JA3-based blocklists keyed on.
Two of Chrome’s extensions are Google inventions and worth naming because they are strong Chromium tells. One is compress_certificate (extension type 27), which advertises support for compressed certificates, with Chrome offering Brotli. The other is application_settings, the ALPS extension, which lets the client and server exchange HTTP/2 SETTINGS during the TLS handshake instead of waiting for the connection to open. ALPS is not something NSS or Apple’s stack sends, so its presence in a ClientHello is a near-certain sign of Chromium underneath. This is the kind of detail that ties the TLS fingerprint to the HTTP/2 layer; the connection between them is the subject of how Cloudflare uses TLS and HTTP/2 fingerprints in bot scoring.
Then came January 2023. Chrome started randomizing the order of its extensions in the ClientHello, shipped through versions 108 and 109 and visible in the wild from January 20, 2023. The pitch from Google’s David Adrian on the Chromium blink-dev list was anti-ossification: a fixed extension order encourages server implementers to fingerprint Chrome and assume specific behavior, which boxes Chrome in. Randomizing the order removes that crutch. The only ordering constraint kept is the one TLS 1.3 mandates, that pre_shared_key, if present, is the last extension. Everything else shuffles per connection. Fastly watched its most common Chrome JA3 (cd08e31494f9531f560d64c695473da9) fall off a cliff starting that day as browsers updated. With around 15 permutable extensions, the number of orderings is close to 15 factorial, on the order of 10^12, so JA3, which hashes extension order directly, effectively gets a fresh value per connection. That single change is why half this blog’s TLS posts exist.
In April 2024, Chrome 124 turned on a post-quantum key share by default, and the ClientHello grew by more than a kilobyte. We will treat the post-quantum transition on its own below because it spans every browser, but the short version is that Chrome led it, shipping the pre-standard X25519Kyber768Draft00 group first and then switching to the standardized X25519MLKEM768 in Chrome 131 in November 2024.
Firefox: the stable one, until post-quantum
Firefox is the conservative counterpart. It ships NSS, and for most of the catalog window its ClientHello stayed comparatively still, which is exactly why Firefox JA3 fingerprints held up far better than Chrome’s. NSS advertises a different cipher order and a different extension set than BoringSSL, so a Firefox handshake is easy to tell apart from a Chrome one at a glance, and historically that telling-apart was stable across long runs of connections.
A representative Firefox cipher list opens with the same three TLS 1.3 suites, then ECDHE-ECDSA and ECDHE-RSA suites with ChaCha20 and AES variants. The extension set carries the Firefox-flavored pieces: delegated_credentials, which Firefox validated and shipped support for in 2019 to let servers use short-lived credentials (capped at seven days’ validity) bound to their CA certificate, and later the privacy extensions around Encrypted Client Hello.
Whether Firefox randomizes its extension order is the one point in this catalog where the public sources disagree, and I will not paper over it. Mozilla has a tracking bug for extension order randomization (Bugzilla 1789436), and several secondary write-ups claim Firefox randomizes from around version 114 onward. Other sources, including write-ups focused specifically on the Chrome change, do not mention Firefox doing it at all, and Firefox JA3 values were still being treated as stable identifiers well after that. The exact current behavior of Firefox extension ordering is not something I can pin to a primary Mozilla source with a version number, so treat any claim that “Firefox randomizes like Chrome” as unconfirmed rather than settled. What is documented is the post-quantum change.
Firefox enabled X25519MLKEM768 by default for HTTPS in Firefox 132, controlled by the security.tls.enable_kyber pref in about:config, and extended it to QUIC and HTTP/3 in Firefox 135. That puts Firefox on the same post-quantum footing as Chrome, give or take a version, by late 2024. The result is that a current Firefox ClientHello, like a current Chrome one, carries a key share over a kilobyte long, and the absence of that key share in something claiming to be a recent Firefox is now a tell.
Two Firefox-specific privacy features round out the modern fingerprint. Firefox shipped Encrypted Client Hello support starting in Firefox 98 and enabled it by default from Firefox 119, which moves the privacy-sensitive extensions into an encrypted ClientHelloInner advertised inside an outer handshake. What ECH does and does not hide is its own subject in Encrypted Client Hello (ECH): what it hides, and the short version for this catalog is that ECH hides the inner SNI but leaves the outer ClientHello fingerprintable, so the browser is still identifiable even when the destination hostname is not.
Safari: Apple’s stack, slow to change
Safari is the odd one out because Apple does not use BoringSSL or NSS. Safari’s TLS comes from Apple’s own networking stack, and its ClientHello reflects Apple’s cipher and extension choices, which are distinct enough that a Safari handshake rarely gets confused for anything else. Apple’s stack supports TLS 1.0 through 1.3 and DTLS, prefers cipher suites with forward secrecy, and offers both AES-128 and AES-256.
The notable property for fingerprinting is stability. Safari has historically not randomized its extension order, so both its JA3 and the normalized JA3N stay constant across connections for a given build, which makes Safari one of the easier browsers to fingerprint reliably and one of the harder ones to impersonate convincingly, because every field has to match a real Apple build. A Safari-on-iOS JA3N observed in the wild looks like:
771,4865-4866-4867-49196-49195-52393-49200-49199-52392-49162-49161-49172-49171-157-156-53-47-49160-49170-10,0-5-10-11-13-16-18-21-23-27-43-45-51-65281,29-23-24-25,0That decodes to legacy version 771 (TLS 1.2 in the version field, with TLS 1.3 offered via supported_versions), a specific ordered cipher list, the extension set, the named groups 29-23-24-25 (X25519, secp256r1, secp384r1, secp521r1), and no EC point format beyond the uncompressed default. The exact byte layout of any one Safari version is not something Apple documents publicly, so the per-version detail here comes from observed traffic rather than a changelog, and Apple’s release notes do not call out ClientHello changes the way Chrome’s blink-dev threads do.
Safari’s post-quantum status lagged the other two. Where Chrome and Firefox had X25519MLKEM768 on by default through 2024, Apple’s rollout came later and across the operating system rather than a single browser version, so a Safari ClientHello without a post-quantum key share was normal for longer than it was for Chrome or Firefox. That makes “no PQC key share” a weaker signal for Safari than for Chrome, which is the sort of per-browser asymmetry that an accurate fingerprint database has to encode rather than assume away.
Edge and the rest of the Chromium family
Microsoft Edge is Chromium. It ships the same BoringSSL that Chrome does and the Chromium-based Edge does not use the Windows SChannel TLS stack for its own connections. The practical consequence is that Edge’s ClientHello is, to first approximation, Chrome’s ClientHello: the same cipher order, the same application_settings and compress_certificate extensions, the same GREASE behavior, the same extension permutation, and the same post-quantum key share on the same rough version cadence. Telling Edge apart from Chrome at the TLS layer alone is genuinely hard, because the layer is identical by construction.
The same holds for the broader Chromium family: Brave, Opera, Vivaldi, Samsung Internet, and the rest inherit BoringSSL’s handshake. Where these browsers differ from Chrome tends to be above TLS, in headers, in HTTP/2 SETTINGS, or in JavaScript-surface behavior, not in the ClientHello. That is why TLS fingerprinting alone collapses the entire Chromium ecosystem into roughly one bucket, and why edge systems combine the TLS fingerprint with the HTTP/2 fingerprint to split it back apart. The HTTP/2 side of that, the SETTINGS frame and the Akamai format, is covered in HTTP/2 fingerprinting.
*Every Chromium browser shares BoringSSL, so they share a ClientHello fingerprint. Firefox and Safari sit in their own buckets because their TLS libraries differ.*GREASE: the 2016 change that has to be filtered out
GREASE is the oldest of the structural changes in this catalog and the one that quietly reshaped how every fingerprinting tool has to parse a ClientHello. Generate Random Extensions And Sustain Extensibility, standardized as RFC 8701, reserves a set of protocol values that a client may advertise to keep servers honest about handling unknown values. The reserved values follow a pattern: cipher suite and ALPN values like {0x0A,0x0A}, {0x1A,0x1A}, on up through {0xFA,0xFA}, and the matching extension and named-group values 0x0A0A, 0x1A1A, through 0xFAFA. A server that sees one of these must ignore it and negotiate with the real values, and a server that chokes on one is a server that would have choked on a genuine future extension.
Google deployed GREASE in Chrome 55 in late 2016, before the RFC was finalized, precisely to gather data on how much of the deployed server base mishandled unknown values. Because GREASE values are random per connection, a fingerprinting tool that naively included them would see a different cipher list or extension list every time. So every serious fingerprint, JA3 included, strips GREASE before hashing. JA4 makes the same rule explicit: ignore GREASE values anywhere they appear, in cipher counts, extension counts, and the hashed lists alike. The presence and placement of GREASE is itself a weak signal (Chrome puts a GREASE extension first and another late in the block), but the values inside are noise by design. The deeper treatment is in GREASE values in the ClientHello.
Extension permutation and the death of JA3
The single most consequential change in this catalog is Chrome’s extension permutation, because it broke the dominant fingerprinting scheme of the previous decade. JA3, introduced by Salesforce engineers in 2017, hashes a comma-joined string of TLS version, cipher list, extension list, named groups, and EC point formats. The extension list is in the order the client sent it. That worked because, before 2023, the order was fixed per browser build. Permutation made the order random, so the same Chrome build now emits a different JA3 on essentially every connection.
The fix the industry converged on has two flavors. JA3N normalizes by sorting the extension list before hashing, which restores a stable value for permuted clients at the cost of throwing away order as a signal. JA4, released by FoxIO in 2023, rebuilds the scheme around sorting from the start and adds discriminating fields that survive permutation. A JA4 fingerprint reads as three sections joined by underscores. The first, ten characters, is human-readable: protocol (t for TLS over TCP, q for QUIC), TLS version (13), an SNI flag (d if SNI is present, i if not), a two-digit cipher count, a two-digit extension count, and two characters from the first ALPN value (h2 for HTTP/2). The second section is a twelve-character truncated SHA-256 of the cipher suites sorted into hex order, GREASE removed. The third is a twelve-character truncated SHA-256 of the extensions, also sorted and GREASE-filtered, with SNI and ALPN themselves excluded from the list, followed by the signature algorithms in their original order. So a JA4 like t13d1516h2_8daaf6152771_e5627efa2ab1 says: TLS over TCP, TLS 1.3, SNI present, 15 ciphers, 16 extensions, ALPN h2, then the two sorted hashes. Sorting is what makes it survive permutation, and the human-readable prefix is what makes it useful for at-a-glance triage.
The migration from one scheme to the other is the subject of TLS fingerprinting: from ClientHello bytes to JA4, and the permutation change specifically is in TLS extension ordering and the Chrome randomization that broke JA3. For this catalog the takeaway is narrow: any per-version fingerprint you record for Chrome 108 or later has to be a sorted one, or it will not match the same browser twice.
The post-quantum transition, 2024 to 2026
The live story in the catalog is post-quantum key exchange, because it is the change currently separating up-to-date browsers from everything pretending to be one. The mechanism is a hybrid key agreement that runs a classical X25519 exchange and a post-quantum ML-KEM-768 encapsulation together, so the session is safe as long as either one holds. ML-KEM-768 is the NIST-standardized form of what was called Kyber-768.
Chrome went first. Chrome 124 enabled the pre-standard X25519Kyber768Draft00 group by default in April 2024, then Chrome 131 switched to the standardized X25519MLKEM768 in November 2024. Firefox enabled X25519MLKEM768 by default in Firefox 132 and added QUIC and HTTP/3 coverage in Firefox 135. Apple’s rollout came across the operating system and landed later than the other two. By late 2025 both Chrome and Firefox offered the hybrid group by default, and Cloudflare reported it negotiated on essentially all of its sites from October 2024.
The byte-level details are what make this a fingerprint rather than a footnote. The pre-standard X25519Kyber768Draft00 group used IANA codepoint 0x6399 (decimal 25497). The standardized X25519MLKEM768 uses 0x11ec (decimal 4588). An ML-KEM-768 public key is 1,184 bytes; X25519 adds 32; the combined client key share runs around 1,216 bytes, where a classic X25519-only share was 32 bytes plus a few bytes of framing. There is a naming quirk worth recording: despite the name X25519MLKEM768, the concatenation order in the key share puts the ML-KEM portion first and X25519 second. The result is a ClientHello that jumps from a few hundred bytes to over 1,400, often spanning two TCP segments, with a key_share and supported_groups that both advertise group 0x11ec.
For anyone running a server-side fingerprint, the post-quantum key share is a gift, because it is expensive to fake. A scraper can rewrite its User-Agent to claim Chrome 131 in a second. It cannot produce a valid 1,216-byte hybrid key share unless its TLS library actually implements ML-KEM, and most do not. So a connection that announces a recent Chrome in HTTP but lacks the post-quantum group in its ClientHello is contradicting itself, and the contradiction is visible at the TLS layer before HTTP starts. Reports put post-quantum key shares on the order of 90-plus percent of real Chrome traffic by 2026, which means the absence of one is now anomalous rather than common. How edge systems capture and act on this is the subject of server-side TLS fingerprinting libraries.
What a current fingerprint looks like, per browser, in 2026
Pulling the catalog together into a snapshot. A current Chrome or any Chromium browser sends a ClientHello around 1,700 bytes, with GREASE in the cipher list and at both ends of a permuted extension block, the application_settings and compress_certificate extensions that mark it as Chromium, and an X25519MLKEM768 key share at group 0x11ec. Its JA3 is effectively random per connection thanks to permutation, so it is identified by a sorted fingerprint like JA4, which lands on one stable value for the build. Edge, Brave, Opera, and the rest of the family produce the same TLS fingerprint and have to be split apart at the HTTP/2 layer or above.
Firefox sends an NSS-flavored handshake, distinct cipher order, the delegated_credentials extension, Encrypted Client Hello on by default since Firefox 119, and the same X25519MLKEM768 key share since Firefox 132. Its fingerprint is its own bucket, easy to separate from Chrome, with the open question being exactly how much its extension order moves per connection. Safari sends an Apple-stack handshake that has historically stayed stable across connections, no extension permutation, a later post-quantum rollout tied to the OS rather than a browser version, which makes a missing post-quantum group a weaker signal for Safari than for the other two.
The thread through all of it is that “looks like Chrome” has a half-life now. Before 2023 a fixed JA3 string identified a browser build for years, and impersonating it meant matching one stable hash. After permutation, after the post-quantum jump, the target moves every few months: a new key share group, a new cipher, a new extension on a new Chrome milestone. The catalog above is a snapshot of mid-2026, and the most reliable thing in it is the rate of change. A fingerprint database that is not refreshed against real, current browser builds drifts out of date on roughly the same cadence Chrome ships, which is every four weeks. The handshake that proves you are a real browser this quarter is evidence you are a stale one next quarter.
Sources & further reading
- Fastly (2023), A First Look at Chrome’s TLS ClientHello Permutation in the Wild — measured the drop-off of the dominant Chrome JA3 starting January 20, 2023.
- David Adrian / Chromium (2022), Ready for Trial: TLS ClientHello extension permutation — the blink-dev thread with Google’s anti-ossification rationale for randomizing extension order.
- net4people/bbs (2023), Google Chrome TLS extension permutation — community notes on which versions shipped permutation and the pre_shared_key constraint.
- IETF (2020), RFC 8701: Applying GREASE to TLS Extensibility — the reserved GREASE values and the rules for advertising and ignoring them.
- FoxIO-LLC (2023), JA4 technical details — the exact JA4 format, GREASE handling, and sorting rules.
- Chrome Platform Status (2023), X25519Kyber768 key encapsulation for TLS — Chrome’s post-quantum feature entry.
- Jan Schaumann (2024), TLS 1.3 Hybrid Key Exchange using X25519Kyber768 / ML-KEM — codepoints, key-share byte sizes, concatenation order, and per-browser deployment.
- Scrapfly (2026), Post-Quantum TLS: Why Scraping Tools Are Now Exposed — how the post-quantum key share exposes clients that spoof a User-Agent without a matching TLS stack.
- lwthiker (2022), Impersonating Chrome, too — Chrome 98 cipher and extension layout, ALPS, and compressed-certificate extensions via BoringSSL.
- Mozilla Security (2021), Encrypted Client Hello: the future of ESNI in Firefox — Firefox ECH support and what moves into the encrypted inner ClientHello.
- Mozilla Security (2019), Validating Delegated Credentials for TLS in Firefox — the delegated_credentials extension and its seven-day validity cap.
- John Althouse / FoxIO (2023), JA4+ Network Fingerprinting — the design goals behind the JA4 suite and why JA3 needed replacing.
Further reading
How Cloudflare uses TLS and HTTP/2 fingerprints in bot scoring
A reference on Cloudflare's network-layer fingerprinting: how JA3, JA4, and the HTTP/2 frame profile are computed at the edge, what cf.bot_management exposes, and how those signals feed the 1-99 bot score.
·23 min readTLS fingerprinting: from ClientHello bytes to JA4
What a ClientHello actually contains, why JA3 worked for six years and then stopped, and what JA4 fixes, with a Python reference you can run against your own packet captures.
·15 min readThe TLS ClientHello, field by field: a fingerprinting reference
A field-by-field dissection of the TLS ClientHello, tracing exactly which bytes JA3 and JA4 read: version, cipher suites, compression, extensions, supported_groups, signature_algorithms, supported_versions, key_share, and ALPN.
·19 min read