JA4 in the Wild: Signal, Drift, and Evasion
Estimated reading time: 18-22 minutes | ~4,200 words
TL;DR
- JA4 is a TLS ClientHello fingerprint with a readable structure and QUIC/ALPN context.
- It fingerprints client software behaviour, not user identity.
- JA4 changes as TLS stacks evolve; treat it as a drifting signal.
- JA4 can be spoofed with tools like curl-impersonate and uTLS; use cross-layer detection (JA4T, HTTP/2, JA4H) to catch inconsistencies.
- Use JA4 for correlation and triage, not as a single hard block.
What JA4 is (and where it sits)
JA4 is a TLS client fingerprint derived from the ClientHello, which is sent in cleartext before encryption begins. It reflects the TLS library and configuration of the client you observe. FoxIO JA4+ overview
JA4 is one member of the broader JA4+ family: JA4S (TLS server fingerprints), JA4H (HTTP clients), JA4L (latency), JA4X (certificates), and more. FoxIO JA4+ overview and APNIC explainer
Where it sits in the stack (text-only diagram)
Client app
|
| TLS ClientHello (cleartext)
v
JA4 computed here (client fingerprint)
|
| TLS termination (proxy/WAF/load balancer) -> may change observed JA4
v
HTTP layer (JA4H) if HTTP is visible after decryption
How to read a JA4 fingerprint
JA4 uses an a_b_c structure to make the fingerprint partially human-readable and analysable by segment. FoxIO JA4+ overview
a(protocol + metadata): protocol identifier (tfor TCP/TLS,qfor QUIC,dfor DTLS), highest supported TLS version (excluding GREASE), SNI presence, cipher count and extension count (both excluding GREASE), and the first and last characters of the first ALPN value (or00if absent). GREASE (Generate Random Extensions And Sustain Extensibility, RFC 8701) values are dummy entries that clients add to prevent servers from ossifying on specific extension lists. JA4 technical detailsb(cipher hash): 12-hex-character truncated SHA-256 hash of the sorted cipher suites (GREASE filtered). NETCAP JA4 formatc(extensions + signature algorithms hash): 12-hex-character truncated SHA-256 hash of the sorted extensions (GREASE, SNI, and ALPN filtered), followed by signature algorithms in their original order if present. JA4 technical details
The canonical specification is published by the JA4 maintainers. FoxIO JA4+ repo
Annotated example:
t13d1516h2_8daaf6152771_e5627efa2ab1
│││ ││││ │ │
│││ ││││ │ └─ c: extensions + sig algos hash
│││ ││││ └─ b: sorted cipher suites hash
│││ ││└┘─ h2: first & last char of ALPN "h2"
│││ │└─ 16: extension count (excluding GREASE)
│││ └─ 15: cipher count (excluding GREASE)
││└─ d: SNI present (domain name provided)
│└─ 13: TLS 1.3
└─ t: TCP/TLS (not QUIC or DTLS)
Real-world JA4 values
Below are real JA4 values verified from (1) the FoxIO sample PCAP chrome-cloudflare-quic-with-secrets.pcapng processed with the official JA4 Python tool, and (2) the FoxIO JA4+ mapping table for Python TLS stacks. Your values will differ by OS, TLS library, and transport.
| Client (verified source) | JA4 example | Notes |
|---|---|---|
| Chrome (FoxIO sample PCAP) | t13d1516h2_8daaf6152771_e5627efa2ab1 | t13d1516h2 decodes as TCP/TLS 1.3, SNI present, 15 ciphers, 16 extensions, ALPN h2. |
| Python TLS (FoxIO mapping table) | t13i181000_85036bcba153_d41ae481755e | Mapping table lists multiple Python variants; expect drift by backend/config. |
Curl note: the public mapping table does not include a curl JA4 (TLS) value, only a curl JA4H (HTTP) value. Curl’s JA4 depends on its TLS backend (OpenSSL vs SecureTransport, etc.), so the most reliable approach is to measure curl in your own environment. FoxIO mapping table
Related fingerprints in the JA4+ family
JA4 is strongest when paired with adjacent signals that cover other layers. At a high level:
- JA4S fingerprints the TLS server response (ServerHello), useful for distinguishing server-side stacks.
- JA4H fingerprints HTTP clients when HTTP is visible after decryption or at plaintext endpoints.
- JA4L captures latency characteristics.
- JA4X fingerprints certificates.
These are designed to be complementary, not replacements. FoxIO JA4+ overview and APNIC explainer
Why JA4 replaced JA3
JA3 (2017) fingerprints TLS clients by hashing ordered ClientHello fields like version, cipher suites, and extensions. Salesforce JA3 origin
In late 2022, Chromium announced ClientHello extension permutation, shipping it in Chrome 110 (early 2023). This made JA3 unstable for browser identification. JA4 addresses this by sorting extensions and adding ALPN/QUIC context, giving a more interpretable and resilient fingerprint for modern TLS and HTTP/3 traffic. Chromium Intent to Ship and Cloudflare JA4 signals
That stability comes with a tradeoff: sorting removes some order-specific entropy. The goal is not maximum uniqueness, but a signal that changes less often for the same client stack. That makes JA4 more practical for monitoring and triage, while still requiring correlation with other telemetry.
What changes JA4 (limits and stability)
Organic drift (expected change)
JA4 is not static. Expect change when:
- TLS libraries and browsers update (new cipher/extension sets).
- Transport changes (TCP/TLS vs QUIC).
- ALPN negotiation changes (h2 vs h3).
- TLS termination shifts (edge proxy, WAF, or load balancer behaviour).
Cloudflare notes that fingerprints can change frequently, and customers sometimes block new fingerprints that are actually benign browser or OS updates. Cloudflare JA4 signals
Capture point matters: a JA4 seen at the CDN edge describes the TLS handshake to the edge, not necessarily the handshake to your origin. If you terminate TLS at multiple layers (edge, internal proxy, origin), you will see different JA4 values for the same end user. Decide which layer you trust for policy and keep that consistent across environments.
Looking forward, Encrypted ClientHello (ECH) will change what passive observers can see; the implications for JA4 are covered in the ECH outlook section below.
Manipulation risk
JA4 fingerprints are not cryptographic proofs. A determined actor can produce a ClientHello that is byte-identical to a legitimate browser, making the resulting JA4 indistinguishable from real traffic.
Two widely used tools demonstrate this:
- curl-impersonate compiles against the actual browser TLS libraries (BoringSSL for Chrome, NSS for Firefox) and replicates their ClientHello byte-for-byte. The resulting JA4 matches a real browser exactly. curl-impersonate
- uTLS (Go) provides programmatic control over every ClientHello field and ships presets for Chrome, Firefox, Safari, and iOS. It generates fingerprints that are indistinguishable from those browsers at the TLS layer. uTLS
Because these tools exist and are straightforward to use, JA4 must be treated as one signal in a multi-layer detection model, not as a standalone block. Cloudflare JA4 signals
Spoofing in practice
curl-impersonate (lexiforest/curl-impersonate, active fork):
# Wrapper scripts are versioned; use one that exists in your install (examples below).
# Chrome/Edge/Safari build:
curl_chrome116 https://example.com
# Firefox build:
curl_firefox135 https://example.com
Verified against tls.browserleaks.com: regular curl produces JA4 t13d4907h2_... (49 ciphers, 7 extensions — obviously not a browser), while a Chrome wrapper (e.g., curl_chrome116) produces t13d1516h2_8daaf6152771_... (15 ciphers, 16 extensions — matching Chrome’s real profile). The Akamai HTTP/2 fingerprint also changes: Chrome’s 1:65536;2:0;4:6291456;6:262144 vs curl’s default 3:100;4:10485760;2:0.
Python (curl_cffi):
# pip install curl_cffi
from curl_cffi import requests
r = requests.get("https://example.com", impersonate="chrome")
print(r.status_code)
uTLS (Go):
package main
import (
"log"
"net"
utls "github.com/refraction-networking/utls"
)
func main() {
tcpConn, err := net.Dial("tcp", "example.com:443")
if err != nil {
log.Fatal(err)
}
defer tcpConn.Close()
uconn := utls.UClient(tcpConn, &utls.Config{ServerName: "example.com"}, utls.HelloChrome_Auto)
if err := uconn.Handshake(); err != nil {
log.Fatal(err)
}
}
curl-impersonate also mimics HTTP/2 SETTINGS frames and pseudo-header ordering per browser. uTLS alone does not control HTTP/2 framing — additional libraries like httpcloak or ooni/oohttp are needed for that layer.
These tools demonstrate why JA4 alone is insufficient. The next section covers how attackers go beyond static spoofing to rotate and construct fingerprints at scale, and what that means for defenders.
Evasion and rotation
The previous section showed that spoofing a single JA4 is straightforward. In practice, sophisticated actors go further: they rotate fingerprints across requests and construct custom ClientHellos that match no known browser.
Fingerprint rotation
Rather than impersonating one browser consistently, evasion tooling cycles through multiple browser presets per session or per request to avoid clustering on a single JA4.
Bash (curl-impersonate) — rotate per request:
#!/usr/bin/env bash
set -euo pipefail
profiles=()
IFS=: read -r -a _path_dirs <<< "${PATH:-}"
for dir in "${_path_dirs[@]}"; do
for p in "$dir"/curl_chrome* "$dir"/curl_firefox* "$dir"/curl_safari*; do
if [ -x "$p" ]; then
profiles+=("$p")
fi
done
done
if [ ${#profiles[@]} -eq 0 ]; then
shopt -s nullglob
profiles=(./curl_chrome* ./curl_firefox* ./curl_safari*)
fi
if [ ${#profiles[@]} -eq 0 ]; then
echo "No curl-impersonate wrappers found in PATH or current directory." >&2
exit 1
fi
for url in "$@"; do
profile=${profiles[$RANDOM % ${#profiles[@]}]}
"$profile" -s "$url" -o /dev/null -w "%{http_code} ${profile##*/} $url\n"
done
Python (curl_cffi) — rotate per request:
# pip install curl_cffi
import random
from curl_cffi import requests
targets = ["chrome", "safari", "safari_ios"]
# If your curl_cffi build supports Firefox, add a supported version (e.g., "firefox133").
urls = ["https://example.com/page1", "https://example.com/page2"]
for url in urls:
# New session per request = new TLS fingerprint each time
r = requests.get(url, impersonate=random.choice(targets))
print(r.status_code, url)
For session-level rotation (same fingerprint across a session’s requests, different fingerprint per session):
import random
from curl_cffi import requests
targets = ["chrome", "safari", "safari_ios"]
session = requests.Session(impersonate=random.choice(targets))
# All requests in this session share one fingerprint
session.get("https://example.com/login")
session.get("https://example.com/dashboard")
Go (uTLS) — rotate across known presets:
package main
import (
"log"
"math/rand"
"net"
"time"
utls "github.com/refraction-networking/utls"
)
func main() {
presets := []utls.ClientHelloID{
utls.HelloChrome_Auto,
utls.HelloFirefox_Auto,
utls.HelloRandomized,
}
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
picked := presets[rng.Intn(len(presets))]
tcpConn, err := net.Dial("tcp", "example.com:443")
if err != nil {
log.Fatal(err)
}
defer tcpConn.Close()
uconn := utls.UClient(tcpConn, &utls.Config{ServerName: "example.com"}, picked)
if err := uconn.Handshake(); err != nil {
log.Fatal(err)
}
}
Go (uTLS) — generate a novel fingerprint per connection:
// HelloRandomized builds a random ClientHello each time.
// Note: some random combinations may fail handshake; retry or
// fall back to a named preset in production use.
package main
import (
"log"
"net"
utls "github.com/refraction-networking/utls"
)
func main() {
tcpConn, err := net.Dial("tcp", "example.com:443")
if err != nil {
log.Fatal(err)
}
defer tcpConn.Close()
uconn := utls.UClient(tcpConn, &utls.Config{ServerName: "example.com"}, utls.HelloRandomized)
if err := uconn.Handshake(); err != nil {
log.Fatal(err)
}
}
HelloRandomized generates a valid but non-deterministic ClientHello, so some combinations may fail against certain servers; retry or fall back to a named preset if a handshake fails. uTLS also provides HelloRandomizedALPN and HelloRandomizedNoALPN variants. uTLS
The standard evasion playbook combines fingerprint rotation with proxy/IP rotation and realistic request timing to avoid both JA4-based and behavioural detection.
Custom fingerprint construction
Beyond rotating known browser presets, attackers can build entirely novel ClientHellos.
uTLS custom ClientHelloSpec (Go):
package main
import (
"log"
"net"
utls "github.com/refraction-networking/utls"
)
func main() {
tcpConn, err := net.Dial("tcp", "example.com:443")
if err != nil {
log.Fatal(err)
}
defer tcpConn.Close()
spec := &utls.ClientHelloSpec{
TLSVersMax: utls.VersionTLS13,
TLSVersMin: utls.VersionTLS12,
CipherSuites: []uint16{
utls.TLS_AES_128_GCM_SHA256,
utls.TLS_CHACHA20_POLY1305_SHA256,
utls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
},
Extensions: []utls.TLSExtension{
&utls.SNIExtension{},
&utls.SupportedCurvesExtension{Curves: []utls.CurveID{
utls.X25519, utls.CurveP256,
}},
&utls.SupportedVersionsExtension{Versions: []uint16{
utls.VersionTLS13, utls.VersionTLS12,
}},
&utls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []utls.SignatureScheme{
utls.ECDSAWithP256AndSHA256,
utls.PSSWithSHA256,
utls.PKCS1WithSHA256,
}},
&utls.KeyShareExtension{KeyShares: []utls.KeyShare{
{Group: utls.X25519},
}},
&utls.ALPNExtension{AlpnProtocols: []string{"h2", "http/1.1"}},
},
}
uconn := utls.UClient(tcpConn, &utls.Config{ServerName: "example.com"}, utls.HelloCustom)
if err := uconn.ApplyPreset(spec); err != nil {
log.Fatal(err)
}
if err := uconn.Handshake(); err != nil {
log.Fatal(err)
}
}
The resulting JA4 matches no known browser, making it invisible to both allowlist and blocklist detection.
curl_cffi custom fingerprint (Python):
# pip install curl_cffi
from curl_cffi import requests
url = "https://example.com"
ja3 = "771,4865-4866-4867-49195-49196-52393-49199-49200-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-51-45-43-21,29-23-24,0"
akamai = "4:16777216|16711681|0|m,p,a,s"
extra_fp = {
"tls_grease": True,
"tls_permute_extensions": True,
"tls_cert_compression": "brotli",
"http2_stream_weight": 256,
}
r = requests.get(url, ja3=ja3, akamai=akamai, extra_fp=extra_fp)
print(r.status_code)
Other tools in this space:
- bogdanfinn/tls-client wraps uTLS with browser presets accessible from Go, Python, and JavaScript.
- httpcloak extends fingerprint matching to HTTP/2 SETTINGS and frame ordering alongside TLS.
What makes rotation detectable
Fingerprint rotation raises the cost for attackers but introduces its own detection surface:
- Intra-session fingerprint changes. Real users do not switch browsers mid-session. A single IP or session presenting multiple distinct JA4 values is a strong bot signal.
- Distribution anomalies. A bot farm drawing uniformly from Chrome, Firefox, and Safari presets looks nothing like real traffic, where Chrome holds ~65% market share. Statistical analysis of fingerprint populations reveals non-human distributions.
- Cross-layer contradictions persist. Rotating the JA4 (TLS layer) does not change the JA4T (TCP layer). A host cycling through Chrome, Firefox, and Safari JA4s while always showing Linux TCP options is trivially flagged.
- Session state leaks. Cookies, auth tokens, or local storage that carry across fingerprint changes betray a single client behind multiple fingerprints.
- Behavioural consistency. Rotating JA4 does not fix request timing patterns, missing JavaScript execution, or mechanical navigation sequences.
Defender takeaway
Fingerprint rotation is the natural escalation after static spoofing, and the tooling is mature. But rotation creates its own anomalies: real users have stable fingerprints, predictable population distributions, and consistent cross-layer signals. Defenders should focus on session-level fingerprint binding, population-level statistical analysis, and the cross-layer detection techniques covered in the next section rather than relying on JA4 values alone.
Defender Playbook: Collection, Correlation, Guardrails
Where to collect JA4 (high level)
Collect JA4 where the ClientHello is visible:
- TLS termination points (edge proxies, WAFs, load balancers).
- Network sensors that parse ClientHello.
- CDN/edge logs that expose JA4 fields.
Platform examples:
- Cloudflare documents JA3/JA4 fingerprints in Bot Analytics and makes them available in Security Events, GraphQL, and Logs (Enterprise Bot Management). Cloudflare JA3/JA4 docs
- Fastly exposes
tls.client.ja4as a request variable. Fastly docs - AWS CloudFront and AWS WAF have announced JA4 support. CloudFront announcement and AWS WAF announcement
Computing JA4
From packet captures:
# JA4 is built into Wireshark/tshark 4.x+ (field: tls.handshake.ja4)
tshark -r capture.pcap -Y tls.handshake.type==1 -T fields -e tls.handshake.ja4
From Zeek logs: JA4 is available via the FoxIO Zeek package. FoxIO JA4+ repo
From CDN logs: Cloudflare, Fastly, and AWS CloudFront all expose JA4 in their log pipelines (see platform examples above).
Defender tooling:
- Fingerproxy — reverse proxy that extracts JA4 + Akamai HTTP/2 fingerprints and forwards them as headers to your backend.
- Finch — fingerprint-aware reverse proxy with block/tarpit rules.
- JA4+ suite — Wireshark and Zeek plugins for the full JA4+ family.
Data model (minimum viable fields)
If you log JA4, log the surrounding context that turns it into a usable signal. A minimal record per request or per TLS session:
{
"ja4": "t13d1516h2_8daaf6152771_e5627efa2ab1",
"ja4t": "t_... (tcp fingerprint)",
"alpn": "h2",
"sni": "example.com",
"tls_version": "TLSv1.3",
"asn": 13335,
"geo": "US",
"user_agent": "Mozilla/5.0 ...",
"http2_settings_hash": "1:65536;3:1000;4:6291456|..."
}
What each field buys you:
ja4: TLS ClientHello fingerprint (from your edge/CDN or sensor). Many providers expose this directly (e.g.,tls.client.ja4at Fastly, JA4 fields in Cloudflare logs, and theCloudFront-Viewer-JA4-Fingerprintheader at AWS). Fastlytls.client.ja4and Cloudflare JA3/JA4 docs and CloudFront header docsja4t: TCP-layer fingerprint (OS/kernel signal) that is much harder to spoof; it helps spot cross-layer mismatches and VPN/proxy artifacts. FoxIO JA4Tsniandtls_version: TLS context from the ClientHello. Edge providers commonly expose these (e.g.,tls.client.servername,tls.client.protocol). Fastlytls.client.servernameand Fastlytls.client.protocoluser_agent,asn,geo: classic context for clustering and false-positive control. These fields are common in CDN/WAF logs (e.g., CloudflareClientRequestUserAgent,ClientSSLProtocol,ClientASN,ClientCountry). Cloudflare HTTP requests dataset and Cloudflare firewall eventshttp2_settings_hash: derived from HTTP/2 SETTINGS/PRIORITY/pseudo-header order; this is the common Akamai-style HTTP/2 fingerprint. It helps catch TLS-only spoofing by checking HTTP/2 consistency. Passive HTTP/2 fingerprinting research and HTTP/2 fingerprinting scheme
Baseline & drift mini-playbook
JA4 drifts with client updates. FoxIO explicitly notes that JA4 fingerprints change as application TLS libraries are updated, roughly on the order of yearly update cycles for major apps. JA4+ Q&A
Practical baseline steps:
- Start with 1-2 weeks of traffic at the same TLS termination layer; compute top JA4s by volume, plus a “new JA4s per day” metric segmented by
tls_version,alpn,asn, andgeo. (This captures normal regional and network variance.) Fastlytls.client.protocoland Cloudflare HTTP requests dataset - Track “new” vs “expected” JA4s with a rolling window. A spike of previously unseen JA4s in a narrow ASN or country is often more actionable than a global drift.
- Separate TCP/TLS vs QUIC baselines. QUIC (
alpn=h3) and TCP/TLS (h2orhttp/1.1) have distinct JA4 populations and should not be mixed. (If you don’t see ALPN because of ECH or logging gaps, treat it as “unknown” rather than “h2.”) TLS ECH draft - Treat missing JA4 as a first-class state. Cloudflare notes JA4 can be absent for non-TLS traffic, for certain internal/Worker routing scenarios, or when Bot Management is skipped. If you only alert on “new JA4,” you may miss whole classes of traffic. Cloudflare JA3/JA4 docs
- Plan change windows around major browser/OS updates and library rollouts. (Rule of thumb: relax thresholds during known update weeks.) Inference based on JA4 drift guidance above.
Action ladder (log → friction → block)
Use JA4 as a confidence amplifier, not a single switch. A simple, safe escalation ladder:
- Log-only (low confidence): new JA4s with no other risk signals; use analytics and clustering to learn populations. Cloudflare JA3/JA4 docs
- Friction (medium confidence): JA4 looks suspicious and one cross-layer mismatch is present (e.g., JA4 says Chrome but JA4T says Linux server). Apply rate limits or lightweight challenges. AWS WAF JA4 support and FoxIO JA4T
- Block (high confidence): repeated abuse with consistent mismatches across TLS + TCP + HTTP/2/headers. Keep an allowlist for known partners and automation you own. Cloudflare JA3/JA4 docs
Correlate with other telemetry
JA4 should be a feature in a broader risk model, not a single blocklist key. Combine it with:
- IP reputation and ASN context
- Device and browser signals (where available)
- Behavioural signals (request cadence, path mix, interaction patterns)
- Server-side signals (auth outcomes, account history)
Use cases and operational patterns
Use JA4 for triage and clustering, not attribution. Practical patterns that work well in production:
- Population inventory: understand which client stacks dominate your traffic.
- Novelty tracking: alert on sudden spikes of previously unseen JA4 values per endpoint or per tenant.
- Rollout validation: detect unexpected shifts after browser or OS updates; treat release weeks as higher-risk for benign drift.
- Cohorting: group requests by JA4 + ALPN + transport to compare behaviour within similar client stacks.
- Incident triage: cluster suspicious traffic by JA4 to scope impact quickly.
False positives to plan for
Expect collisions and benign shifts:
- Shared egress (NAT, carrier networks): many users share the same JA4 because client stacks are common; carrier networks can also shift TCP characteristics. FoxIO JA4T
- Corporate proxies & TLS inspection: diverse clients collapse into a few JA4s because the proxy terminates TLS and re-issues ClientHello on behalf of users.
- VPNs / tunnels: TCP MSS and options may change due to overhead, producing distinct JA4T values even when JA4 looks “normal.” FoxIO JA4T
- Version rollouts: JA4 changes as TLS libraries update; expect drift during release windows. JA4+ Q&A
Testing in a lab (owned environment)
Use a controlled test setup to understand drift and reduce surprises:
- Baseline JA4 for known clients and versions.
- Track changes across browser, OS, and TLS library updates.
- Test both TCP/TLS and QUIC paths.
- Record expected drift windows in change management.
- Measure false positives before enforcing hard blocks.
ECH outlook (what changes for JA4)
Encrypted ClientHello (ECH) encrypts sensitive ClientHello fields, including SNI and ALPN, inside an inner ClientHello. Passive observers only see the outer ClientHello, which is designed to be innocuous and privacy-preserving. TLS ECH draft
Practical implications:
- Passive sensors lose visibility: if you compute JA4 from a passive tap, you may see “outer” fingerprints with less SNI/ALPN detail. Expect fewer distinct JA4 values in passive data. Inference based on ECH design.
- Terminating edges retain visibility: if you terminate TLS (or terminate ECH) at your edge, you can still compute JA4 from the inner ClientHello and log it normally. Inference based on ECH design.
- Plan for split populations: the same client population can look different at passive sensors vs edge termination points; treat them as separate baselines. TLS ECH draft
Cross-layer detection
Because JA4 can be spoofed at the TLS layer, defenders should cross-check against signals from other layers that are harder to forge.
Quick checks:
- TLS vs TCP: JA4 says “Chrome on Windows” but JA4T looks like Linux server options.
- TLS vs headers: Chrome JA4 with missing
sec-ch-ua*headers (in secure contexts) is suspicious. - TLS vs behaviour: Stable JA4 but highly mechanical request timing or navigation patterns.
JA4T (TCP fingerprinting): TCP SYN parameters are set by the OS kernel, not the application. A Chrome JA4 originating from a Linux server will still show Linux TCP options (MSS -> SACK -> TS -> NOP -> WS), while real Chrome on Windows shows Windows TCP options without timestamps. curl-impersonate and uTLS cannot spoof JA4T. FoxIO reports JA4T alone can block over 80% of internet scan traffic. FoxIO JA4T
HTTP/2 frame analysis: Real browsers have characteristic HTTP/2 SETTINGS values. A comparison:
| Parameter | Chrome | Go default | Python |
|---|---|---|---|
| INITIAL_WINDOW_SIZE | 6,291,456 | 65,535 | ~64 KB |
| HEADER_TABLE_SIZE | 65,536 | 4,096 | varies |
curl-impersonate matches these values. uTLS alone does not control HTTP/2 framing. Akamai HTTP/2 fingerprinting whitepaper
HTTP header consistency (JA4H): Real Chromium browsers send sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform (Client Hints) and sec-fetch-* metadata headers by default in secure contexts. Their absence with a Chrome JA4 is a strong bot signal (with the caveat that WebViews and non-HTTPS contexts may legitimately omit them). Header ordering also differs between real browsers and automated tools.
Common mismatch patterns:
| TLS Fingerprint | Observed Signal | Verdict |
|---|---|---|
| Chrome JA4 | Linux TCP stack (JA4T) | Likely bot on Linux server |
| Chrome JA4 | No sec-ch-ua headers | Missing Client Hints |
| Chrome JA4 | HTTP/2 window size 65,535 | uTLS without HTTP/2 matching |
| iPhone/Safari UA | Linux JA4T | Bot impersonating iOS |
The principle is straightforward: spoofing one layer is easy, but maintaining consistency across TCP, TLS, HTTP/2, HTTP headers, and behaviour is hard.
Policy and ethics
Document where JA4 is collected, how it is used, and retention limits. Use it for defence and monitoring in systems you own or operate. Avoid irreversible decisions based on JA4 alone.
Licensing note
JA4 (TLS client fingerprinting) is BSD-3-Clause licensed, while other JA4+ methods (e.g., JA4S, JA4H, JA4X, JA4SSH) fall under the FoxIO License, which restricts monetization without an OEM license. FoxIO JA4+ overview and NETCAP license note
Defender checklist
- Log JA4 at TLS termination points.
- Normalise and store JA4 alongside behaviour and account signals.
- Separate baselines for TCP/TLS and QUIC.
- Track drift around browser/TLS library updates.
- Treat sudden changes as investigation triggers, not automatic blocks.
- Account for shared egress (NAT, mobile, proxies).
- Validate how ALPN shifts affect fingerprints.
- Test in a controlled lab before enforcing rules.
- Keep policy and retention documentation current.
- Review false positives regularly and adjust thresholds.
Privacy considerations
What JA4 can reveal
- Client TLS stack characteristics (protocol, version, cipher/extension sets, ALPN).
- Broad client software families (shared TLS library behaviour).
What JA4 cannot reveal
- Application payload contents (JA4 is computed before encryption).
- A stable individual identity by itself.
What users can do without evasion
- Prefer mainstream, patched browsers to avoid uncommon stacks.
- Be aware of enterprise TLS inspection and what it implies for privacy.
- Treat privacy claims that ignore network-layer metadata with skepticism.
If you build products that use JA4, consider communicating it in your security or privacy documentation, and set reasonable retention limits for fingerprint data.
FAQ
Can VPNs change JA4? A VPN that only routes traffic may not change the ClientHello. A VPN or proxy that terminates and re-originates TLS will change the observed JA4.
Does changing User-Agent change JA4? Not directly. JA4 is derived from TLS ClientHello, while User-Agent is an HTTP header.
How stable is JA4 over time? More stable than JA3 against extension-order randomization, but it still drifts with browser, OS, and TLS library changes.
Can JA4 identify individual users? Not by itself. JA4 fingerprints client stacks, not people, and collisions are common (shared egress, corporate proxies, popular browsers).
Should I block unknown JA4s? Usually no. Treat unknowns as novelty signals, enrich with other telemetry, and escalate via the action ladder rather than hard-blocking.
Does JA4 cover QUIC/HTTP/3?
Yes. QUIC fingerprints are represented in JA4 with the q prefix and h3 ALPN; keep separate baselines for TCP/TLS and QUIC.
Will ECH make JA4 useless? It reduces passive visibility (outer ClientHello only), but if you terminate TLS/ECH at your edge you can still compute JA4 from the inner ClientHello. Treat passive and edge datasets separately.
Conclusion
JA4 is a practical fingerprint for understanding client stacks, but it is not identity. Its value comes from combining it with TCP, HTTP, and behavioural signals, and from treating drift as normal rather than suspicious by default. The strongest programs pair JA4 with baselines, change windows, and an escalation ladder that applies friction only when multiple signals align. Used carefully, JA4 improves triage and monitoring without overreaching.
Sources consulted
- FoxIO: JA4+ Network Fingerprinting (official). https://blog.foxio.io/ja4%2B-network-fingerprinting
- FoxIO: JA4+ technical spec repository. https://github.com/FoxIO-LLC/ja4
- FoxIO: JA4 technical details (field definitions). https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md
- FoxIO: JA4+ mapping table. https://github.com/FoxIO-LLC/ja4/blob/main/ja4plus-mapping.csv
- Cloudflare: JA4 signals and field breakdown. https://blog.cloudflare.com/ja4-signals
- Cloudflare: JA3/JA4 documentation. https://developers.cloudflare.com/bots/additional-configurations/ja3-ja4-fingerprint/
- Chromium: Intent to Ship TLS ClientHello extension permutation. https://groups.google.com/a/chromium.org/g/blink-dev/c/bYZK81WxYBo/m/CCl6Y2qLBAAJ
- NETCAP: JA4 format and license notes. https://docs.netcap.io/master/tls-fingerprinting
- APNIC: JA4+ network fingerprinting (independent explainer). https://blog.apnic.net/2023/11/22/ja4-network-fingerprinting/
- Salesforce: JA3 origin and methodology. https://engineering.salesforce.com/open-sourcing-ja3-92c9e53c3c41
- Fastly:
tls.client.ja4documentation. https://www.fastly.com/documentation/reference/vcl/variables/client-connection/tls-client-ja4/ - AWS: CloudFront JA4 support announcement. https://aws.amazon.com/about-aws/whats-new/2024/10/amazon-cloudfront-ja4-fingerprinting/
- AWS: WAF JA4 support announcement. https://aws.amazon.com/about-aws/whats-new/2025/03/aws-waf-ja4-fingerprinting-aggregation-ja3-ja4-fingerprints-rate-based-rules/
- IETF: Encrypted ClientHello (ECH) draft (latest version). https://datatracker.ietf.org/doc/draft-ietf-tls-esni/
- curl-impersonate (original): browser-impersonating curl builds. https://github.com/lwthiker/curl-impersonate
- curl-impersonate (active fork): https://github.com/lexiforest/curl-impersonate
- curl_cffi (Python): https://github.com/lexiforest/curl_cffi
- uTLS: programmable TLS ClientHello for Go. https://github.com/refraction-networking/utls
- FoxIO: JA4T TCP fingerprinting. https://blog.foxio.io/ja4t-tcp-fingerprinting
- Akamai: Passive fingerprinting of HTTP/2 clients (Black Hat EU 2017). https://blackhat.com/docs/eu-17/materials/eu-17-Shuster-Passive-Fingerprinting-Of-HTTP2-Clients-wp.pdf
- bogdanfinn/tls-client: uTLS wrapper for Go/Python/JS. https://github.com/bogdanfinn/tls-client
- Fingerproxy: https://github.com/wi1dcard/fingerproxy
- Finch: https://github.com/0x4D31/finch