Why JA3 Stopped Being Enough

Cloudflare has long relied on fingerprinting characteristics of client software to identify what kind of device or script is making an HTTP request—a technique central to products like the DDoS engine, WAF, and Bot Management. These fingerprints distinguish a browser from a Python script efficiently and accurately while preserving user privacy. But in early 2023, Google introduced a change to Chromium-based browsers that shuffled the order of TLS extensions, disrupting the JA3 fingerprinting method that many security tools, including Cloudflare's, depended on.

The JA3 hash, introduced by Salesforce researchers in 2017, created a unique identifier by hashing the TLS ClientHello message—including the ordered list of cipher suites and extensions. Google's randomization strategy made JA3 unreliable for identifying unique clients. Beyond this, three structural weaknesses accelerated JA3's decline:

  • Randomization of TLS extensions: Browsers began randomizing the order of TLS extensions in ClientHello, causing JA3 fingerprints to vary per connection.
  • Inconsistencies across tools: Different implementations and databases produced divergent results, undermining reliable cross-organization sharing and threat intelligence.
  • Limited scope: JA3 focused only on the TLS ClientHello packet—a narrow slice of the OSI model—and did not include newer transport protocols like QUIC.

The JA4 Approach

FoxIO's JA4, launched in September 2023, provides a more robust and adaptable successor. It is part of the broader JA4+ suite covering TLS, HTTP, and SSH fingerprints. JA4 resists TLS extension randomization and adds dimensions like Application Layer Protocol Negotiation (ALPN), making it useful across modern protocols including QUIC. It is available under the BSD 3-Clause license, facilitating upgrades from JA3. Cloudflare has integrated JA4 into its platform, joining many open-source and commercial tools.

A JA4 fingerprint for the latest Google Chrome on Linux shows the various components that make up the identifier:

2361-2
  1. Protocol identifier (t): Indicates TLS over TCP, distinguished from q for QUIC and d for DTLS.
  2. TLS version (13): TLS 1.3, derived from the highest version in the ClientHello, excluding GREASE values.
  3. SNI presence (d): A domain name is present in the Server Name Indication; i would indicate its absence.
  4. Cipher suites count (15): Total cipher suites in ClientHello, excluding GREASE.
  5. Extensions count (16): Distinct extensions present, indicating supported functionalities.
  6. ALPN values (h2): The negotiated protocol—HTTP/2.
  7. Cipher hash (8daaf6152771): A truncated SHA256 hash of cipher suites sorted in hexadecimal order.
  8. Extension hash (02713d6af862): A truncated SHA256 hash of sorted extensions combined with signature algorithms.

A Wireshark capture from Chrome on Linux querying https://www.cloudflare.com illustrates a real ClientHello:

2361-3

A Unified Parser in Rust

Integrating JA4 required reworking how Cloudflare parses TLS ClientHello messages. Previously, separate implementations existed in C, Lua, and Go. Cloudflare developed a new Rust-based internal crate, client-hello-parser, which centralizes all ClientHello parsing logic. This supports a future transition from nginx to a Rust-based service and exposes JA4 fingerprints across firewall rules, Workers, and analytics.

The crate populates a ClientHelloParsed struct with version 1 and version 2 fingerprints plus JA3 and JA4 hashes. Its key design goals:

  • Optimized memory usage: Amortized zero heap allocations, using the dhat crate for verification and tiny_vec for stack-allocated small vectors.
  • Memory safety: Rust's borrow checker combined with fuzzing has resolved vulnerabilities previously undetected in C implementations.
  • Ultra-low latency: The faster_hex crate uses SIMD instructions for hex encoding; Rust iterators often compile to SIMD-optimized assembly. BigEndianIterator streams TLS ClientHello bytes in a single pass.
client_hello_benchmark/parse/parse-short-502
                        time:   [497.15 ns 497.23 ns 497.33 ns]
                        thrpt:  [2.0107 Melem/s 2.0111 Melem/s 2.0115 Melem/s]
client_hello_benchmark/parse/parse-long-1434
                        time:   [992.82 ns 993.55 ns 994.99 ns]
                        thrpt:  [1.0050 Melem/s 1.0065 Melem/s 1.0072 Melem/s]

Benchmarks show the parser handling shorter ClientHello messages at roughly 2 million elements per second and longer ones at around 1 million.

  • Robust testing: Dozens of real TLS ClientHello examples are verified against Wireshark with JA3 and JA4 plugins. The Cargo fuzzer with memory sanitizer ensures no leaks, and backward compatibility tests with the legacy C parser via FFI confirm equivalent results.
  • Seamless nginx integration: The crate compiles as a dynamic library linked to nginx, easing the transition from the legacy parser.

Retiring multiple language-specific parsers has improved performance and edge-case robustness. It also enables simpler integration of new parsing features.

Adding Context for Customers

With JA4 deployed, Cloudflare faced a separate problem. When JA3 was introduced, customers sometimes blocked new fingerprints that turned out to be legitimate browser or OS updates. A bare hash lacks context, leaving customers to make blind decisions. Cloudflare wanted to provide the context needed for informed, confident actions on fingerprint safety. As more customers integrate AI, they are also demanding access to the raw signals behind bot detection, to run their own models on proprietary data while still drawing on Cloudflare's visibility into Internet-wide traffic patterns. Both use cases point to the same need: fingerprints alone are not enough—inter-request context matters.

Beyond the Fingerprint: Inter-Request Signals

JA3 and JA4 fingerprints have become standard tools for identifying and categorizing web traffic. But as stand-alone signals, they have limits: fingerprints can be spoofed, they rotate frequently, and they describe only a single connection. Malicious actors exploit these gaps. JA4 Signals close them.

JA4 Signals are inter-request features computed from the last hour of traffic across Cloudflare's global network. The scale is substantial: daily analysis of more than 15 million unique JA4 fingerprints, generated from over 500 million user agents and billions of IP addresses. This aggregated view reveals traffic patterns that a single request—or even a single fingerprint—cannot convey.

The signals are available in the Firewall Rules UI, Bot Analytics, and Workers, enabling custom rules, rate-limiting rules, transform rules, or Workers logic. Consider a concrete case from a Firewall events activity log involving the latest version of Chrome on Linux:

JA4 Signal

Description

Value example

Interpretation

browser_ratio_1h

The ratio of requests originating from browser-based user agents for the JA4 fingerprint in the last hour. Higher values suggest a higher proportion of browser-based requests.

0.942

Indicates a 94.2% browser-based request rate for this JA4.

cache_ratio_1h

The ratio of cacheable responses for the JA4 fingerprint in the last hour. Higher values suggest a higher proportion of responses that can be cached.

0.534

Shows a 53.4% cacheable response rate for this JA4.

h2h3_ratio_1h

The ratio of HTTP/2 and HTTP/3 requests combined with the total number of requests for the JA4 fingerprint in the last hour. Higher values indicate a higher proportion of HTTP/2 and HTTP/3 requests compared to other protocol versions.

0.987

Reflects a 98.7% rate of HTTP/2 and HTTP/3 requests.

reqs_quantile_1h

The quantile position of the JA4 fingerprint based on the number of requests across all fingerprints in the last hour. Higher values indicate a relatively higher number of requests compared to other fingerprints.

1

High volume of requests compared to other JA4s.

The HTTP request in question received a Bot Score of 95, indicating it likely came from a human user rather than an automated client. The JA4 Signals contextualize this score by showing how this particular client behaves relative to other network clients with similar fingerprints. These fields give customers deeper insight into any request they inspect.

The following Worker script demonstrates one way to parse and categorize JA4 Signals, setting up a structure for further analysis or rule application:

/**
 * Event listener for 'fetch' events. This triggers on every request to the worker.
 */
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

/**
 * Main handler for incoming requests.
 * @param {Request} request - The incoming request object from the fetch event.
 * @returns {Response} A response object with JA4 Signals in JSON format.
 */
async function handleRequest(request) {
  // Safely access the ja4Signals object using optional chaining, which prevents errors if properties are undefined.
  const ja4Signals = request.cf?.botManagement?.ja4Signals || {};

  // Construct the response content, including both the original ja4Signals and the parsed signals.
  const responseContent = {
    ja4Signals: ja4Signals,
    jaSignalsParsed: parseJA4Signals(ja4Signals)
  };

  // Return a JSON response with appropriate headers.
  return new Response(JSON.stringify(responseContent), {
    status: 200,
    headers: {
      "content-type": "application/json;charset=UTF-8"
    }
  })
}

/**
 * Parses the JA4 Signals into categorized groups based on their names.
 * @param {Object} ja4Signals - The JA4 Signals object that may contain various metrics.
 * @returns {Object} An object with categorized JA4 Signals: ratios, ranks, and quantiles.
 */
function parseJA4Signals(ja4Signals) {
  // Define the keys for each category of signals.
  const ratios = ['h2h3_ratio_1h', 'heuristic_ratio_1h', 'browser_ratio_1h', 'cache_ratio_1h'];
  const ranks = ['uas_rank_1h', 'paths_rank_1h', 'reqs_rank_1h', 'ips_rank_1h'];
  const quantiles = ['reqs_quantile_1h', 'ips_quantile_1h'];

  // Return an object with each category containing only the signals that are present.
  return {
    ratios: filterKeys(ja4Signals, ratios),
    ranks: filterKeys(ja4Signals, ranks),
    quantiles: filterKeys(ja4Signals, quantiles)
  };
}

/**
 * Filters the keys in the ja4Signals object that match the list of specified keys and are not undefined.
 * @param {Object} ja4Signals - The JA4 Signals object.
 * @param {Array<string>} keys - An array of keys to filter from the ja4Signals object.
 * @returns {Object} A filtered object containing only the specified keys that are present in ja4Signals.
 */
function filterKeys(ja4Signals, keys) {
  const filtered = {};
  // Iterate over the specified keys and add them to the filtered object if they exist in ja4Signals.
  keys.forEach(key => {
    // Check if the key exists and is not undefined to handle optional presence of each signal.
    if (ja4Signals && ja4Signals[key] !== undefined) {
      filtered[key] = ja4Signals[key];
    }
  });
  return filtered;
}

Why Aggregated Signals Matter

  • Broader context for analysis: Aggregating data over an hour provides a holistic view of traffic, making it easier to spot emerging threats by tracking changes over time rather than examining isolated requests.
  • Sharper anomaly detection: Inter-request features can reveal anomalies that single-request fingerprinting misses, leading to more accurate identification of sophisticated attacks.
  • Harder to manipulate: Synthesizing data at global scale makes the system more resilient to spoofing and manipulation than analysis based on individual requests.
  • Flexible enforcement: Signals can feed everything from simple firewall configurations to complex machine learning models, allowing security rules to adapt as traffic patterns shift.
  • Fewer false positives and negatives: With richer context, security systems can better distinguish legitimate traffic from malicious activity, improving reliability.

The Speed Advantage

The efficiency of JA4 fingerprint computation enables real-time threat detection, while the aggregated statistics and inter-request features in JA4 Signals provide deep insight at microsecond speeds. This combination underpins Cloudflare's Bot Management and DDoS protection offerings.

These capabilities rest on the scalable techniques and open-sourced libraries described in "Every request, every microsecond: scalable machine learning at Cloudflare". The network processes over 50 million requests per second, and JA4 Signals turn that volume into actionable, dynamic security measures.

Enterprise customers with bot challenges—or those running internal threat models—can benefit from access to this data. Contact Cloudflare for more on the Bot Management offering.