A TLS fingerprint is a compact description of a client's TLS handshake. It can help distinguish a standard browser connection from a script or reveal a mismatch between a claimed browser and the network client making the request. It does not identify a person, prove that a request is automated, or explain what the client intends to do.
JA3 and JA4 are two ways to turn fields from a TLS ClientHello into a value
that can be compared across connections. JA3 preserves the order of several
lists. JA4 sorts cipher and extension IDs before hashing them, so extension
permutation does not produce a new hash for those parts.
Start with the ClientHello
The ClientHello is the client's opening TLS handshake message. In TLS 1.3 it
contains a legacy version field, a random value, a session ID, offered cipher
suites, compression methods, and extensions. The extensions can describe
supported TLS versions, groups, signature algorithms, key shares, the server
name, and application protocols such as HTTP/2. The exact structure is defined
in RFC 8446.
An abbreviated observation might look like this:
transport: TLS over TCP
highest supported version: TLS 1.3
cipher suites: 1301, 1302, 1303, ...
extensions: server_name, supported_groups, alpn, key_share, ...
first ALPN value: h2
Those fields reflect choices made by the client's TLS stack and its configuration. Different programs may produce different combinations. The same program can also change its handshake after an update, on another operating system, or when configured differently.
The collection point matters. The original ClientHello is available where
the visitor's TLS connection terminates. If a CDN terminates that connection
and opens a new one to your origin, the origin sees the CDN's handshake, not
the visitor's. Collect the fingerprint at the trusted TLS boundary and carry
it forward as connection metadata if the application needs it.
From handshake to fingerprint
A practical pipeline has six steps:
- Capture the
ClientHelloat the TLS termination point. - Parse only the bounded fields required by the chosen fingerprint format.
- Remove or normalize values exactly as that format specifies.
- Calculate the fingerprint and record the format and implementation version.
- Attach it to the connection or request without treating it as authenticated identity.
- Compare it with other evidence available for the same request and session.
Do not invent a private variation and still call it JA3 or JA4. A lookup is only portable when producers use the same field selection, GREASE handling, sorting, and hashing rules.
JA3 and JA4 compared
| Property | JA3 | JA4 |
|---|---|---|
| Primary input | TLS version, offered ciphers, extensions, supported groups, and EC point formats | Transport and protocol metadata, counts, ALPN, ciphers, extensions, and signature algorithms |
| List order | Preserves cipher, extension, group, and point-format order | Sorts cipher IDs and extension IDs; signature algorithms remain in their presented order |
| GREASE | Excludes GREASE values | Excludes GREASE values |
| Output | MD5 of the comma-and-hyphen-delimited input | A readable prefix plus two 12-character truncated SHA-256 hashes |
| Useful property | Simple, widely recognized representation of the observed list order | More stable when a client permutes cipher or extension order |
| Main limitation | Harmless ordering changes can create a different value | Sorting removes ordering information; shared or imitated handshakes still look alike |
Salesforce's JA3 specification defines the five input fields and the ordered string that is hashed. Its implementation also ignores GREASE values so reserved placeholders do not become new fingerprints. Salesforce archived the repository on May 1, 2025, and no longer maintains it. The specification still describes how existing JA3 values were produced.
Reading a JA4 string
FoxIO's JA4 specification
builds a value in three parts, a_b_c, such as
t13d1516h2_8daaf6152771_b186095e22b6:
| Part | Value | Meaning |
|---|---|---|
| a, character 1 | t | TLS over TCP. q means QUIC and d means DTLS |
| a, characters 2 and 3 | 13 | TLS version, here TLS 1.3 |
| a, character 4 | d | SNI extension present. i means no SNI |
| a, characters 5 and 6 | 15 | Cipher suites offered, GREASE excluded |
| a, characters 7 and 8 | 16 | Extensions offered, GREASE excluded |
| a, last two characters | h2 | First and last characters of the first ALPN value. 00 means no ALPN |
| b | 8daaf6152771 | First 12 characters of a SHA-256 hash of the sorted cipher list |
| c | b186095e22b6 | First 12 characters of a SHA-256 hash of the sorted extensions, without SNI and ALPN, followed by the signature algorithms in their original order |
Because the parts are separate, you can compare one without the others.
FoxIO's README gives the example of
tracking a client that rotates ciphers by matching JA4_ac, which keeps parts
a and c and drops b. JA4 itself is BSD 3-Clause, like JA3, while most other
JA4+ methods use the FoxIO License.
Modern Chromium can permute TLS extensions in the ClientHello to reduce
protocol ossification. Chromium's own feature definition describes the
permutation behavior. Chromium source
An order-sensitive JA3 can therefore change across connections even when the
underlying client has not. JA4's sorting makes its cipher and extension hashes
stable under that particular change. It does not make JA4 permanent or
unspoofable. Some scraping browsers, such as Ulixee Hero, document emulation of browser network and TLS
characteristics for this reason.
What GREASE means here
GREASE values are reserved placeholders that clients can send at TLS extension points. They are intended to expose implementations that fail when they see an unknown value, keeping those extension points usable. They are not invalid bytes and their presence is not proof of Chrome or any other browser. RFC 8701 defines the mechanism and the reserved values.
Both JA3 and JA4 exclude GREASE values as part of their normalization. Keep the raw, access-controlled observation separately if GREASE behavior matters to your analysis; it will not be represented fully in the normalized hash.
Reproduce a fingerprint locally
Wireshark exposes both tls.handshake.ja3 and tls.handshake.ja4. JA4 is
available in Wireshark 4.2 and later, according to its TLS display-filter reference.
Use this small lab only on a device and traffic you are allowed to inspect:
- Record
tshark --version,curl --version, the browser version, operating system, destination host, and test time. - Start a Wireshark capture on the interface carrying your traffic.
- Make three new TLS-over-TCP connections to the same test host with system curl. Then make three with a normal browser. Avoid a host for which the browser already has a reusable connection.
- Stop the capture and save it as
tls-lab.pcapng. - Extract the fingerprint fields:
tshark -r tls-lab.pcapng \
-Y "tls.handshake.type == 1" \
-T fields \
-e tcp.stream \
-e tls.handshake.ja3 \
-e tls.handshake.ja4 \
-e tls.handshake.ja3_full
Put the six rows in a table with the client, version, run number, JA3, and JA4. Compare repeated runs before comparing the two clients. You may see changing JA3 values with a stable JA4 when extension order is permuted. You may instead see stable values for both. The result depends on the exact client, build, configuration, and route.
This is a protocol demonstration, not a detection benchmark. Six local connections cannot establish false-positive rates, population frequency, or whether a fingerprint belongs to a bot. HTTP/3 uses QUIC rather than a TCP stream, so keep the first lab to TLS over TCP or adapt the capture and JA4 transport interpretation deliberately.
Use lookup data as context, not identity
A fingerprint database can tell you that a value has previously appeared with a program, browser family, malware sample, or internal client. The record needs provenance: who observed it, with which implementation, on what date, and under which configuration.
There are two different kinds of ambiguity:
- Many-to-one: different users or programs can produce the same normalized fingerprint. A common browser fingerprint should be expected across many unrelated people.
- One-to-many: one program can produce different fingerprints after an update, across platforms, or under different TLS settings.
Hash collisions are also possible in principle. JA3 uses MD5 and JA4 truncates SHA-256 outputs, but these values are labels, not cryptographic proof. In operations, shared inputs and changing inputs are usually the more immediate source of mistaken attribution.
Age every lookup entry. Keep an unknown state. A fingerprint that is absent from the database is not automatically suspicious, and an old label should not silently become a permanent block rule.
What a TLS fingerprint cannot prove
A matching fingerprint cannot prove that the request came from the named
browser. Tools can deliberately imitate browser handshakes.
curl-impersonate changes curl's
TLS and HTTP/2 behavior to resemble supported browsers.
uTLS exposes low-level
ClientHello control and browser-like presets. Both projects also document
limits: configuration changes can alter a signature, and ClientHello
parroting does not reproduce every later behavior of a browser.
A TLS fingerprint also cannot answer:
- whether a human or automation controls the client;
- whether a request is benign, abusive, or authorized;
- whether two connections belong to the same actor;
- whether a claimed crawler is operated by the company named in its user agent;
- whether the client will behave like the imitated browser after the handshake.
For a named crawler, use the operator's published verification method rather than substituting a TLS label. How to verify AI agents covers those methods.
Combine TLS with independent evidence
TLS fingerprinting is most useful as a consistency check. It can raise or lower confidence when compared with evidence from other layers.
| Observation | Useful comparison | Caution |
|---|---|---|
| Browser user agent with an unfamiliar TLS fingerprint | HTTP protocol behavior, browser APIs, navigation sequence | A browser update or unusual platform may be legitimate |
| Familiar browser JA4 across many sessions | Session behavior, request graph, cookies, challenge result | Common browsers naturally share fingerprints |
| Claimed named crawler | Published IP or DNS verification, documented token, resource policy | User-agent and TLS labels are not operator identity |
| TLS fingerprint changes mid-session | Connection reuse, proxy path, network change, session continuity | Mobile networks and multi-edge routes can change connections |
| Browser-like TLS plus browser-like HTTP | Runtime and session evidence | Impersonation tools can reproduce more than the handshake |
The decision model should preserve which layer contributed each signal. A single mismatch may justify observation or a step-up check. Blocking needs a policy that accounts for the route, confidence, impact on legitimate readers, and rollback.
The next layer is covered in How to detect browser automation.
Operational checklist
- Collect at the boundary that sees the visitor's original
ClientHello. - Version the parser and fingerprint format.
- Bound parsing work and retain an explicit unknown state.
- Store source, date, client version, and configuration with lookup labels.
- Re-test known fingerprints after browser, TLS library, CDN, or proxy changes.
- Keep observation separate from enforcement.
- Measure false positives on your own traffic before blocking.
- Log the rule version and reason so a decision can be audited and reversed.
Centinel helps teams combine connection, request, browser, and session evidence when classifying automated traffic. Evaluate it with traffic from your own site and verify which signals are available at your TLS termination point. To establish a baseline before enforcing a rule, request a site audit.
