JA4 Fingerprinting Against AI Scrapers: A Practical Guide

TLS fingerprinting is experiencing a renaissance. The reason is simple: Browser-as-a-Service platforms like Browserbase, Hyperbrowser, and the growing ecosystem of LLM-powered browsers can spoof nearly every JavaScript API, rotate residential IPs, and generate convincing user agents. What they cannot easily fake is the TLS handshake.

This guide walks through JA4—the modern successor to JA3—with practical examples from real AI scraping tools. Whether you are a security analyst investigating suspicious traffic or a platform engineer building detection systems, this is the technical reference you need.

Why TLS Fingerprinting Matters Now

The bot detection landscape has shifted dramatically. Traditional signals are compromised:

User-Agent strings: Trivially spoofed. Every BaaS platform rotates user agents automatically.

JavaScript environment: Stealth libraries like Puppeteer Extra patch navigator.webdriver, fake plugins arrays, spoof canvas fingerprints, and override dozens of browser APIs. These patches are well-documented and widely deployed.

IP reputation: Residential proxy networks are cheap and abundant. Traffic originates from ISP-assigned addresses that pass reputation checks.

Behavioral patterns: AI agents are getting better at mimicking human interaction timing. While still detectable, the signal is weaker than it was.

TLS fingerprinting exploits a fundamental asymmetry: spoofing the TLS handshake requires recompiling the TLS stack. You cannot just change a header or override a JavaScript property. The cipher suites, extensions, and protocol parameters are baked into the client implementation.

When Browserbase runs a stealth session claiming to be Chrome 120, the TLS handshake reveals the actual Chromium version of their cloud infrastructure. When an LLM browser built on Playwright makes requests, its TLS fingerprint matches Playwright—not Chrome.

This is the detection vector BaaS platforms cannot easily neutralize.

JA3: The Foundation

Before diving into JA4, understanding JA3 provides essential context. Developed by Salesforce in 2017, JA3 was the first widely-adopted TLS fingerprinting method.

How JA3 Works

JA3 creates a fingerprint from the TLS ClientHello message by concatenating five fields:

  1. TLS Version (2 bytes) - The protocol version offered
  2. Cipher Suites (variable) - Encryption algorithms in preference order
  3. Extensions (variable) - TLS extensions in order
  4. Elliptic Curves (variable) - Supported key exchange curves
  5. EC Point Formats (variable) - Elliptic curve point format support

These values are joined with commas and hyphens, then MD5 hashed:

TLSVersion,Ciphers,Extensions,EllipticCurves,ECPointFormats

Example raw string:

771,4866-4865-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-21,29-23-24-25,0

MD5 hash:

cd08e31494f9531f560d64c695473da9

JA3 Limitations

JA3 served well for years but has significant limitations:

GREASE randomization: Modern browsers use GREASE (Generate Random Extensions And Sustain Extensibility) values that change between sessions. These are dummy values designed to prevent protocol ossification. JA3 includes them in the hash, meaning the same browser can produce different fingerprints.

Extension order sensitivity: JA3 captures extension order, but browsers can reorder extensions without functional impact. This creates unnecessary fingerprint variance.

TLS 1.3 challenges: TLS 1.3 encrypts more of the handshake. Some parameters visible in TLS 1.2 are now hidden.

Evasion libraries: Tools like uTLS allow programmatic manipulation of TLS parameters. An attacker can construct a ClientHello that produces any desired JA3 hash.

These limitations motivated the development of JA4.

JA4: Next-Generation TLS Fingerprinting

JA4 was released in 2023 by FoxIO to address JA3’s shortcomings. It represents a fundamental rethinking of TLS fingerprinting methodology.

The JA4 Family

JA4 is actually a suite of fingerprinting methods:

FingerprintTargetUse Case
JA4TLS ClientHelloPrimary client identification
JA4STLS ServerHelloServer configuration analysis
JA4HHTTP headersApplication-layer fingerprinting
JA4XX.509 certificatesCertificate chain analysis
JA4TTCP parametersNetwork stack identification
JA4SSHSSH handshakeSSH client fingerprinting

For AI scraper detection, JA4 and JA4H are the most relevant.

JA4 Structure

Unlike JA3’s opaque MD5 hash, JA4 uses a human-readable format with three sections:

[protocol][version][SNI][cipher_count][extension_count][ALPN]_[cipher_hash]_[extension_hash]

A real JA4 fingerprint looks like:

t13d1516h2_8daaf6152771_b0da82dd1658

Breaking this down:

Section 1: t13d1516h2

  • t - TCP (vs q for QUIC)
  • 13 - TLS 1.3
  • d - Domain SNI present (vs i for IP)
  • 15 - 15 cipher suites offered
  • 16 - 16 extensions present
  • h2 - HTTP/2 ALPN (Application-Layer Protocol Negotiation)

Section 2: 8daaf6152771

  • Truncated SHA256 of sorted cipher suite list

Section 3: b0da82dd1658

  • Truncated SHA256 of sorted extension list

Why JA4 Is Harder to Evade

GREASE filtering: JA4 strips GREASE values before hashing. Random padding no longer affects the fingerprint.

Sorted hashing: Cipher suites and extensions are sorted before hashing. Reordering no longer changes the fingerprint.

Readable format: The prefix provides immediate context without database lookups. You can see at a glance: TLS version, transport protocol, and connection characteristics.

Multiple dimensions: Combining JA4 with JA4H creates a multi-layered fingerprint that requires spoofing both TLS and HTTP layers correctly.

Real Fingerprints from AI Scraping Tools

Let us examine actual fingerprints from the tools security analysts encounter in production.

Browserbase Sessions

Browserbase runs managed Chromium instances for AI agents and web automation. Despite claiming various Chrome versions in the User-Agent, their sessions produce consistent TLS fingerprints.

Observed JA4 fingerprint:

t13d1517h2_8daaf6152771_02713d6af862

Analysis:

  • t13 - TLS 1.3 over TCP
  • d - Proper SNI handling
  • 15 - 15 cipher suites (matches Chromium)
  • 17 - 17 extensions (slightly different from stock Chrome)
  • h2 - HTTP/2 negotiation

The extension count differs from stock Chrome builds because Browserbase’s environment modifies TLS configuration. When a session claims Chrome/121.0.0.0 but presents 17 extensions instead of Chrome 121’s standard 16, this mismatch is a detection signal.

JA4H fingerprint:

ge11nn060000_c48a6182b93a_c48a6182b93a_0000000000000000

The HTTP header fingerprint reveals:

  • ge - GET method
  • 11 - 11 headers present
  • nn - No cookies, no referer
  • Standard Accept-* header patterns

Real Chrome sessions show different JA4H patterns, particularly around header ordering and cookie presence.

Playwright-Based AI Browsers

AI agents built on Playwright (including many open-source LLM browsers) share Playwright’s TLS characteristics.

Observed JA4 fingerprint:

t13d1516h2_8daaf6152771_e5627efa2ab1

The cipher hash matches Chromium (8daaf6152771), but the extension hash differs (e5627efa2ab1). This occurs because Playwright’s launch configuration modifies extension handling.

Specifically, Playwright often:

  • Disables certain extensions for stability
  • Adds automation-related extensions
  • Modifies extension order for performance

These modifications are invisible at the JavaScript layer (stealth patches hide them) but visible in the TLS handshake.

Python Requests Library

When AI agents fall back to direct HTTP requests (common for API scraping), Python’s requests library has a distinctive fingerprint.

JA4 fingerprint (Python 3.11 + requests):

t12d1307h1_c16a28f6ef30_0000000000000000

Analysis:

  • t12 - TLS 1.2 (Python’s ssl module defaults)
  • 13 - 13 cipher suites
  • 07 - 7 extensions (minimal)
  • h1 - HTTP/1.1 only (no HTTP/2)
  • Empty extension hash indicates no SNI extensions

This fingerprint is trivially detectable. Any traffic claiming to be Chrome but presenting this fingerprint is definitively not Chrome.

curl and wget

Command-line tools used for testing and scripting have distinct fingerprints:

curl 7.x JA4:

t12d1309h1_c35a2a7e3d2f_0000000000000000

wget JA4:

t12d0907h1_b8ea3a52c2bc_0000000000000000

Both show:

  • TLS 1.2 preference
  • Minimal extension support
  • HTTP/1.1 only
  • Low cipher suite counts

Go HTTP Client

Go’s net/http package has a recognizable fingerprint:

Go 1.21 JA4:

t13d1310h2_9dc936c68ed4_000000000000
  • TLS 1.3 support
  • 13 cipher suites
  • 10 extensions
  • HTTP/2 capable

Go clients claiming to be browsers are immediately detectable by this fingerprint.

Node.js (undici/fetch)

Modern Node.js uses undici for HTTP:

Node 20 JA4:

t13d1411h2_7b5a4dc2bc8e_d43e45c10a9f
  • TLS 1.3
  • 14 cipher suites
  • 11 extensions
  • HTTP/2

The cipher and extension hashes differ significantly from browser implementations.

Building a JA4 Detection System

Here is a practical architecture for security teams implementing JA4-based detection.

Data Collection

Capturing JA4 requires TLS handshake visibility. Options include:

Reverse proxy with TLS termination:

# nginx with ssl_preread for JA4 extraction
stream {
    server {
        listen 443 ssl;
        ssl_preread on;

        # Log TLS parameters for JA4 generation
        access_log /var/log/nginx/tls_fingerprints.log tls_fingerprint;
    }
}

Load balancer integration:

  • HAProxy: ssl_fc_sni, ssl_fc_cipher variables
  • AWS ALB: TLS metadata in access logs
  • Cloudflare: JA4 available via cf.bot_management.ja4 (see Cloudflare JA4 section below)

Dedicated TLS inspection:

# Using scapy for packet capture
from scapy.all import sniff
from scapy.layers.tls.handshake import TLSClientHello

def extract_ja4(packet):
    if packet.haslayer(TLSClientHello):
        hello = packet[TLSClientHello]
        # Extract cipher suites
        ciphers = [c.name for c in hello.ciphers]
        # Extract extensions
        extensions = [e.type for e in hello.ext]
        # Generate JA4
        return generate_ja4(hello.version, ciphers, extensions)

Fingerprint Database

Maintain a database mapping fingerprints to known clients:

CREATE TABLE tls_fingerprints (
    id SERIAL PRIMARY KEY,
    ja4 VARCHAR(50) NOT NULL,
    ja4h VARCHAR(100),
    client_name VARCHAR(100),
    client_version VARCHAR(50),
    is_browser BOOLEAN DEFAULT FALSE,
    is_automation BOOLEAN DEFAULT FALSE,
    is_known_bot BOOLEAN DEFAULT FALSE,
    threat_score INTEGER DEFAULT 0,
    first_seen TIMESTAMP DEFAULT NOW(),
    last_seen TIMESTAMP DEFAULT NOW(),
    occurrence_count INTEGER DEFAULT 1,
    notes TEXT
);

-- Index for fast lookups
CREATE INDEX idx_ja4 ON tls_fingerprints(ja4);
CREATE INDEX idx_ja4h ON tls_fingerprints(ja4h);

-- Sample entries
INSERT INTO tls_fingerprints (ja4, client_name, is_browser, is_automation) VALUES
('t13d1516h2_8daaf6152771_b0da82dd1658', 'Chrome 120', TRUE, FALSE),
('t13d1517h2_8daaf6152771_02713d6af862', 'Browserbase', FALSE, TRUE),
('t13d1516h2_8daaf6152771_e5627efa2ab1', 'Playwright', FALSE, TRUE),
('t12d1307h1_c16a28f6ef30_0000000000000000', 'Python requests', FALSE, TRUE);

Detection Logic

class JA4Detector:
    def __init__(self, db_connection):
        self.db = db_connection
        self.cache = {}

    def analyze_request(self, ja4: str, ja4h: str, user_agent: str) -> dict:
        """
        Analyze a request for fingerprint anomalies.

        Returns:
            dict with threat_score, signals, and verdict
        """
        signals = []
        threat_score = 0

        # 1. Check known fingerprint database
        known = self.lookup_fingerprint(ja4)
        if known:
            if known['is_known_bot']:
                signals.append({
                    'type': 'known_bot_fingerprint',
                    'detail': known['client_name'],
                    'confidence': 95
                })
                threat_score += 40

            if known['is_automation'] and 'Chrome' in user_agent:
                signals.append({
                    'type': 'automation_claiming_browser',
                    'detail': f"{known['client_name']} claiming {user_agent}",
                    'confidence': 90
                })
                threat_score += 45

        # 2. Analyze JA4 prefix for anomalies
        prefix_analysis = self.analyze_ja4_prefix(ja4)
        if prefix_analysis['anomalies']:
            signals.extend(prefix_analysis['anomalies'])
            threat_score += prefix_analysis['score_boost']

        # 3. Cross-reference with User-Agent
        ua_consistency = self.check_ua_consistency(ja4, user_agent)
        if not ua_consistency['consistent']:
            signals.append({
                'type': 'ja4_ua_mismatch',
                'detail': ua_consistency['detail'],
                'confidence': ua_consistency['confidence']
            })
            threat_score += int(ua_consistency['confidence'] * 0.5)

        # 4. Check JA4H if available
        if ja4h:
            http_analysis = self.analyze_ja4h(ja4h, user_agent)
            signals.extend(http_analysis['signals'])
            threat_score += http_analysis['score_boost']

        # Determine verdict
        if threat_score >= 80:
            verdict = 'block'
        elif threat_score >= 50:
            verdict = 'challenge'
        elif threat_score >= 25:
            verdict = 'flag'
        else:
            verdict = 'allow'

        return {
            'threat_score': min(threat_score, 100),
            'signals': signals,
            'verdict': verdict,
            'ja4': ja4,
            'ja4h': ja4h
        }

    def analyze_ja4_prefix(self, ja4: str) -> dict:
        """Analyze the human-readable JA4 prefix."""
        anomalies = []
        score_boost = 0

        # Parse prefix: t13d1516h2
        prefix = ja4.split('_')[0]

        # Extract components
        transport = prefix[0]  # t or q
        tls_version = prefix[1:3]  # 13, 12, 11, 10
        sni = prefix[3]  # d or i
        cipher_count = int(prefix[4:6])
        ext_count = int(prefix[6:8])
        alpn = prefix[8:]  # h1, h2, etc.

        # Check for outdated TLS
        if tls_version in ['10', '11']:
            anomalies.append({
                'type': 'outdated_tls',
                'detail': f'TLS 1.{tls_version[-1]} is deprecated',
                'confidence': 80
            })
            score_boost += 25

        # Check for HTTP/1.1 only (unusual for modern browsers)
        if alpn == 'h1':
            anomalies.append({
                'type': 'no_http2_support',
                'detail': 'Client does not support HTTP/2',
                'confidence': 60
            })
            score_boost += 15

        # Check for low extension count (automation tools)
        if ext_count < 10:
            anomalies.append({
                'type': 'low_extension_count',
                'detail': f'Only {ext_count} TLS extensions (browsers have 15+)',
                'confidence': 70
            })
            score_boost += 20

        return {
            'anomalies': anomalies,
            'score_boost': score_boost
        }

    def check_ua_consistency(self, ja4: str, user_agent: str) -> dict:
        """Check if JA4 fingerprint is consistent with claimed User-Agent."""

        # Known browser JA4 patterns (cipher hash portion)
        chrome_cipher_hash = '8daaf6152771'
        firefox_cipher_hash = '5b6e3c2d1a9f'
        safari_cipher_hash = '3d4e5f6a7b8c'

        # Extract cipher hash from JA4
        parts = ja4.split('_')
        if len(parts) >= 2:
            cipher_hash = parts[1]
        else:
            return {'consistent': True, 'detail': 'Unable to parse JA4', 'confidence': 0}

        # Check claimed browser vs actual fingerprint
        if 'Chrome' in user_agent and cipher_hash != chrome_cipher_hash:
            return {
                'consistent': False,
                'detail': f'Claims Chrome but cipher hash is {cipher_hash}',
                'confidence': 85
            }

        if 'Firefox' in user_agent and cipher_hash != firefox_cipher_hash:
            return {
                'consistent': False,
                'detail': f'Claims Firefox but cipher hash is {cipher_hash}',
                'confidence': 85
            }

        return {'consistent': True, 'detail': None, 'confidence': 0}

Alert Integration

Feed detection results into your security infrastructure:

def send_to_siem(detection_result: dict, request_metadata: dict):
    """Send detection event to SIEM."""

    event = {
        'timestamp': datetime.utcnow().isoformat(),
        'event_type': 'tls_fingerprint_detection',
        'source_ip': request_metadata['client_ip'],
        'destination': request_metadata['host'],
        'user_agent': request_metadata['user_agent'],
        'ja4': detection_result['ja4'],
        'ja4h': detection_result.get('ja4h'),
        'threat_score': detection_result['threat_score'],
        'verdict': detection_result['verdict'],
        'signals': detection_result['signals'],
        'severity': 'high' if detection_result['threat_score'] >= 80 else 'medium'
    }

    # Splunk HEC
    if SPLUNK_ENABLED:
        requests.post(
            SPLUNK_HEC_URL,
            headers={'Authorization': f'Splunk {SPLUNK_TOKEN}'},
            json={'event': event}
        )

    # Elastic
    if ELASTIC_ENABLED:
        es_client.index(
            index='security-tls-fingerprints',
            document=event
        )

Analysis Workflows for Security Teams

Investigating Suspicious Traffic

When you identify potentially automated traffic, JA4 analysis follows this workflow:

Step 1: Extract fingerprints from logs

# Parse nginx logs for JA4 data
grep "ja4=" /var/log/nginx/access.log | \
  awk -F'ja4=' '{print $2}' | \
  cut -d' ' -f1 | \
  sort | uniq -c | sort -rn | head -20

Step 2: Identify anomalous patterns

-- Find fingerprints claiming Chrome but not matching Chrome's signature
SELECT
    ja4,
    user_agent,
    COUNT(*) as requests,
    COUNT(DISTINCT source_ip) as unique_ips
FROM access_logs
WHERE user_agent LIKE '%Chrome%'
  AND ja4 NOT IN (SELECT ja4 FROM known_chrome_fingerprints)
GROUP BY ja4, user_agent
ORDER BY requests DESC;

Step 3: Cross-reference with known automation tools

-- Match against automation fingerprint database
SELECT
    l.ja4,
    l.user_agent,
    k.client_name as detected_client,
    COUNT(*) as request_count
FROM access_logs l
JOIN tls_fingerprints k ON l.ja4 = k.ja4
WHERE k.is_automation = TRUE
GROUP BY l.ja4, l.user_agent, k.client_name
ORDER BY request_count DESC;

Building Detection Rules

Cloudflare WAF Rule (using JA4):

(cf.bot_management.ja4 eq "t12d1307h1_c16a28f6ef30_0000000000000000" and
 http.user_agent contains "Chrome")
or
(cf.bot_management.ja4 contains "t13d1517h2_8daaf6152771" and
 cf.bot_management.score lt 30)

For a full walkthrough of Cloudflare JA4 rules, including rate limiting, composite rules, and troubleshooting, see the Cloudflare JA4 Fingerprinting section below.

HAProxy ACL:

# Block known automation fingerprints
acl automation_ja4 req.fhdr(x-ja4) -m str t13d1517h2_8daaf6152771_02713d6af862
acl automation_ja4 req.fhdr(x-ja4) -m str t13d1516h2_8daaf6152771_e5627efa2ab1
acl automation_ja4 req.fhdr(x-ja4) -m str t12d1307h1_c16a28f6ef30_0000000000000000

http-request deny if automation_ja4

Reporting Dashboard Metrics

Track these JA4-related metrics:

  • Fingerprint diversity: Unique JA4 hashes per day
  • Mismatch rate: Requests where JA4 contradicts User-Agent
  • Automation percentage: Traffic from known automation fingerprints
  • New fingerprint alerts: Previously unseen JA4 hashes
  • Block rate by fingerprint: Effectiveness of fingerprint-based rules

Cloudflare JA4 Fingerprinting: Complete Guide

Cloudflare is the most common place security teams encounter JA4 fingerprints in practice. If you manage a site behind Cloudflare and have Bot Management enabled, you already have access to JA4 data for every request. This section covers how to find it, read it, and use it in firewall rules.

What cf.bot_management.ja4 Means

Cloudflare exposes JA4 fingerprints through the cf.bot_management.ja4 field. This field is available in WAF custom rules, Workers, and log exports for customers on plans that include Bot Management.

The value is the full JA4 fingerprint string, formatted exactly as described earlier in this guide:

t13d1516h2_8daaf6152771_b0da82dd1658

When you see this field in Cloudflare logs or rule expressions, it represents the TLS ClientHello fingerprint that Cloudflare computed at their edge when the client first connected. Cloudflare performs TLS termination at their edge nodes, so they have direct access to the raw handshake data. No additional configuration is required on your end.

Key points about cf.bot_management.ja4:

  • Computed at the edge: Cloudflare generates the fingerprint before the request reaches your origin server
  • Available per-request: Each HTTP request includes the JA4 fingerprint of the TLS session it arrived on
  • Not spoofable by the client: Since Cloudflare computes it from the actual TLS handshake, clients cannot inject a fake value
  • Requires Bot Management: This field is only populated on plans that include Bot Management (Enterprise, or Business with the add-on)

If you are on a Free or Pro plan, cf.bot_management.ja4 will be empty. You can still use cf.bot_management.ja3_hash on those plans, though JA3 is less reliable for the reasons covered in the JA3 section above.

Cloudflare JA4 Fingerprint Format Explained

The JA4 format in Cloudflare follows the standard JA4 specification. Here is a complete breakdown using a real example:

t13d1516h2_8daaf6152771_b0da82dd1658
│││ ││││││ │            │
│││ │││││└─ ALPN: h2 (HTTP/2)
│││ ││││└── Extension count: 16
│││ │││└─── Cipher suite count: 15
│││ ││└──── (part of cipher count)
│││ │└───── SNI type: d (domain name present)
│││ └────── TLS version: 13 (TLS 1.3)
││└──────── (part of TLS version)
│└───────── Transport: t (TCP)
└────────── Section separator (_)
SectionValueMeaning
tTransport protocolt = TCP, q = QUIC
13TLS version13 = TLS 1.3, 12 = TLS 1.2
dSNI indicatord = domain SNI present, i = IP-based
15Cipher suite countNumber of cipher suites in ClientHello
16Extension countNumber of TLS extensions offered
h2ALPN valueh2 = HTTP/2, h1 = HTTP/1.1, h3 = HTTP/3
8daaf6152771Cipher hashTruncated SHA256 of sorted cipher suites
b0da82dd1658Extension hashTruncated SHA256 of sorted extensions

Reading the prefix at a glance: A fingerprint starting with t13d15 tells you this is a TLS 1.3 client connecting over TCP with a domain SNI and 15 cipher suites. That is consistent with a modern Chromium-based browser. If you see t12d0907h1 instead, you are looking at a TLS 1.2 client with only 9 cipher suites and no HTTP/2 support, which is likely a scripting tool like wget or an outdated HTTP library.

Common JA4 Fingerprints You Will See in Cloudflare

Here is a reference table of fingerprints that frequently appear in Cloudflare Bot Management logs:

JA4 PrefixLikely ClientNotes
t13d1516h2Chrome (desktop)Standard Chrome with 15 ciphers, 16 extensions
t13d1517h2Browserbase / modified ChromiumExtra extension compared to stock Chrome
t13d1516h2 (different ext hash)PlaywrightSame cipher hash as Chrome, different extension hash
t13d1411h2Node.js (undici)14 ciphers, 11 extensions
t13d1310h2Go net/http13 ciphers, 10 extensions
t12d1307h1Python requestsTLS 1.2, no HTTP/2, minimal extensions
t12d1309h1curlTLS 1.2, 13 ciphers, 9 extensions
t12d0907h1wgetTLS 1.2, very few ciphers and extensions
q13d1516h3Chrome (QUIC/HTTP3)Same as Chrome but over QUIC transport

The cipher hash (8daaf6152771) stays consistent across Chromium-based clients because they share the same cipher suite list. The extension hash is where you spot modifications introduced by automation frameworks.

How to Find JA4 Data in the Cloudflare Dashboard

Step 1: Open Security Events

Navigate to Security > Events in your Cloudflare dashboard. Click on any individual request event to expand its details.

Step 2: Check the Bot Management section

In the expanded event view, look for the Bot Management panel. This displays:

  • Bot Score: Cloudflare’s overall bot probability (1 = definitely bot, 99 = definitely human)
  • JA3 Hash: The legacy JA3 fingerprint (MD5)
  • JA4 Fingerprint: The JA4 string (if Bot Management is active)
  • Verified Bot: Whether the request matches a known good bot (Googlebot, etc.)

Step 3: Use the JA4 value in analysis

Copy the JA4 fingerprint from any suspicious request. You can then:

  1. Search for the same JA4 across all recent events to see how many requests share that fingerprint
  2. Filter by cf.bot_management.ja4 in the analytics view to measure traffic volume per fingerprint
  3. Use the prefix to quickly classify the client type without needing a lookup database

Step 4: Export for deeper analysis

If you use Cloudflare Logpush, the BotManagementJa4 field is available in HTTP request logs. Push these to your SIEM (Splunk, Elastic, Datadog) for historical analysis and correlation with other security signals.

Cloudflare JA4 Firewall Rules: Practical Examples

Cloudflare WAF custom rules use a domain-specific expression language. Here are production-ready rules for common JA4-based detection scenarios.

Rule 1: Block known automation fingerprints

This rule blocks requests from Python requests, curl, and wget that are pretending to be browsers:

(cf.bot_management.ja4 eq "t12d1307h1_c16a28f6ef30_0000000000000000" and
 http.user_agent contains "Chrome")
or
(cf.bot_management.ja4 eq "t12d1309h1_c35a2a7e3d2f_0000000000000000" and
 http.user_agent contains "Mozilla")
or
(cf.bot_management.ja4 eq "t12d0907h1_b8ea3a52c2bc_0000000000000000" and
 http.user_agent contains "Chrome")

Action: Block

This is safe to deploy because a legitimate Chrome browser will never produce a t12d prefix with 7-9 extensions and HTTP/1.1 only. The mismatch between claimed User-Agent and actual TLS fingerprint is definitive.

Rule 2: Challenge Browserbase and Playwright sessions

Rather than blocking outright (which could catch edge cases), challenge suspected BaaS traffic:

(cf.bot_management.ja4 contains "t13d1517h2_8daaf6152771" and
 cf.bot_management.score lt 30)
or
(cf.bot_management.ja4 contains "t13d1516h2_8daaf6152771_e5627efa2ab1")

Action: Managed Challenge

This targets Browserbase (17 extensions instead of 16) and Playwright (known extension hash) while combining with the bot score to reduce false positives.

Rule 3: Flag TLS 1.2 traffic claiming to be modern browsers

Modern Chrome, Firefox, and Edge all negotiate TLS 1.3. Any request claiming a 2024+ browser version but connecting over TLS 1.2 deserves scrutiny:

(cf.bot_management.ja4 starts_with "t12" and
 (http.user_agent contains "Chrome/12" or
  http.user_agent contains "Chrome/13" or
  http.user_agent contains "Firefox/12" or
  http.user_agent contains "Edge/12"))

Action: Managed Challenge

Rule 4: Rate-limit by JA4 fingerprint

Cloudflare rate limiting rules can use JA4 as a counting dimension. This is powerful because rotating IPs no longer bypasses the rate limit if the TLS fingerprint stays the same:

(cf.bot_management.ja4 eq "t13d1517h2_8daaf6152771_02713d6af862")

Action: Rate Limit (e.g., 20 requests per 10 seconds per fingerprint)

This effectively rate-limits all Browserbase sessions collectively, regardless of which IP or session ID they use.

Rule 5: Log-only rule for fingerprint discovery

Before blocking anything, deploy a log-only rule to understand your JA4 traffic distribution:

(cf.bot_management.ja4 ne "" and
 cf.bot_management.score lt 50)

Action: Log

Review the logged events in Security > Events to build your allowlist and blocklist before enforcing any actions.

Combining JA4 with Other Cloudflare Bot Management Fields

JA4 is most effective when combined with other Cloudflare signals:

FieldWhat It Tells YouBest Combined With
cf.bot_management.ja4TLS client identityhttp.user_agent for mismatch detection
cf.bot_management.scoreOverall bot probabilityja4 for high-confidence blocking
cf.bot_management.verified_botKnown good botsExclude from JA4 rules (Googlebot, etc.)
cf.bot_management.ja3_hashLegacy TLS fingerprintFallback for older analysis databases
cf.threat_scoreIP reputationCombine with JA4 for defense in depth

A strong composite rule looks like this:

(cf.bot_management.score lt 20 and
 cf.bot_management.ja4 starts_with "t12" and
 not cf.bot_management.verified_bot and
 http.user_agent contains "Chrome")

This targets requests that score as likely bots, use an outdated TLS version, are not verified good bots, and claim to be Chrome. Each signal individually could produce false positives. Together, they are highly specific.

JA4 vs JA3 in Cloudflare: Which Should You Use?

If you have access to both fields, prefer JA4 for new rules. Here is why:

AspectJA3 (cf.bot_management.ja3_hash)JA4 (cf.bot_management.ja4)
FormatOpaque MD5 hashHuman-readable prefix + hashes
GREASE handlingAffected by GREASE randomizationGREASE values stripped
Extension orderOrder-sensitiveSorted before hashing
Quick classificationRequires database lookupPrefix tells you TLS version, cipher count, ALPN
False positive rateHigher (GREASE changes fingerprint)Lower (more stable across sessions)
Rule authoringMust match exact hash stringsCan use starts_with and contains on prefix

In practice, JA4’s human-readable prefix is a significant advantage for rule authoring. Instead of maintaining a list of opaque MD5 hashes, you can write rules that match patterns: “block any TLS 1.2 client with fewer than 10 extensions claiming to be Chrome.”

JA3 is still useful when working with older threat intelligence feeds that reference JA3 hashes. Many published IOCs (Indicators of Compromise) still use JA3 notation. Running both in parallel during a transition period is reasonable.

Troubleshooting Cloudflare JA4 Rules

JA4 field is empty: You need Bot Management enabled on your plan. Free and Pro plans do not populate this field. Check Security > Bots in the dashboard to verify your Bot Management status.

Fingerprint does not match expected value: Cloudflare terminates TLS at their edge. If a client connects through another proxy or CDN before reaching Cloudflare, you will see the intermediate proxy’s fingerprint, not the original client’s. This is common with clients behind corporate proxies or VPN services.

Rule is not triggering: Use the Rule Preview feature in the Cloudflare dashboard to test your expression against recent traffic. Also verify that the rule priority is correct. Cloudflare evaluates WAF custom rules in order, and a preceding “allow” rule could skip your JA4 check.

High false positive rate: Start with Managed Challenge instead of Block. Monitor the challenge solve rate. If legitimate users are solving challenges at a high rate, your JA4 pattern is too broad. Narrow it by adding additional conditions (bot score, path matching, or User-Agent checks).

Evasion Techniques and Countermeasures

Current Evasion Methods

uTLS library: Allows Go programs to mimic arbitrary TLS fingerprints:

import (
    tls "github.com/refraction-networking/utls"
)

// Mimic Chrome's TLS fingerprint
config := &tls.Config{...}
conn, _ := tls.Dial("tcp", "example.com:443",
    config, &tls.ClientHelloID{
        Client:  "Chrome",
        Version: "120",
    })

Countermeasure: Combine JA4 with behavioral analysis. Even with perfect TLS mimicry, automation exhibits detectable interaction patterns.

TLS proxy chaining: Route traffic through a browser-based proxy to inherit its fingerprint.

Countermeasure: Analyze latency patterns. Proxy-chained requests show characteristic timing signatures.

Browser farm services: Use actual browser instances to establish TLS connections, then inject automation.

Countermeasure: Monitor for impossible behavior—no human clicks 1000 times per second with perfect accuracy.

Defense in Depth

JA4 is one layer of a comprehensive detection system:

  1. TLS fingerprinting (JA4): Network layer, hard to spoof
  2. HTTP fingerprinting (JA4H): Application layer correlation
  3. JavaScript environment checks: Detect stealth patches
  4. Behavioral analysis: Interaction pattern detection
  5. Honeypots: Definitive automation signals

When all layers agree the client is legitimate, confidence is high. When layers disagree—Chrome User-Agent, Playwright JA4, synthetic mouse movements—the verdict is clear.

WebDecoy’s JA4 Implementation

WebDecoy integrates JA4 fingerprinting as a core detection signal. Our SDKs for Node.js, Go, and PHP automatically extract TLS parameters and generate JA4/JA4H fingerprints.

Detection flow:

Request → TLS Handshake → Extract JA4

                    Compare against database

                    Cross-reference with User-Agent

                    Add to threat score

                    Combine with behavioral signals

                    Verdict: allow/challenge/block

What we detect:

  • Browserbase sessions claiming browser User-Agents
  • Playwright/Puppeteer automation
  • Python/Go/Node HTTP clients
  • curl/wget command-line tools
  • Unknown automation with low TLS extension counts
  • Version mismatches between claimed and actual browsers

Integration:

const WebDecoy = require('@webdecoy/node-sdk');

const webdecoy = new WebDecoy({
  apiKey: 'your-api-key',
  enableTLSFingerprinting: true,
  tlsAnalysis: {
    checkJA4: true,
    checkJA4H: true,
    blockKnownAutomation: true,
    alertOnMismatch: true
  }
});

app.use(webdecoy.middleware());

Conclusion

TLS fingerprinting via JA4 provides a detection vector that BaaS platforms cannot easily neutralize. While they can patch JavaScript APIs, rotate IPs, and generate convincing user agents, the TLS handshake reveals the underlying client implementation.

For security analysts, JA4 offers:

  • Immediate classification: Human-readable prefix provides instant context
  • Database correlation: Match against known automation tools
  • Mismatch detection: Compare fingerprint against claimed browser
  • Evasion resistance: Sorted hashing defeats randomization

For platform engineers, JA4 integrates into:

  • Firewall rules: Block known automation fingerprints
  • Threat scoring: Add TLS signals to multi-factor detection
  • SIEM alerting: Feed fingerprint mismatches to security operations
  • Forensics: Investigate suspicious traffic patterns

The cat-and-mouse game continues. Evasion libraries like uTLS raise the bar. But defense in depth—JA4 combined with behavioral analysis, honeypots, and environment validation—maintains detection advantage.

AI scrapers built on BaaS infrastructure cannot hide their nature forever. The TLS handshake tells the truth.


Ready to add JA4 detection to your security stack?

Start Your Free Trial and see WebDecoy’s TLS fingerprinting in action. Our SDKs handle extraction, analysis, and alerting automatically.

Questions about implementing JA4 analysis? Read our documentation or contact us directly.


Related Resources:

Want to see WebDecoy in action?

Get a personalized demo from our team.

Request Demo