The Short Answer Up Front
JA4 is the dominant TLS fingerprint signal anti-bot vendors are consuming in 2026. It is also one of the most misunderstood signals in the automation community, because guides routinely conflate two unrelated problems: "my Playwright script is being blocked" and "my Python HTTP client is being blocked."
Playwright launches a real Chromium binary. The TLS handshake is performed by Chromium's BoringSSL, against the real cipher and extension order Chrome ships in production. The JA4 hash that comes out is the JA4 hash of real Chrome — because it is real Chrome doing the handshake. There is no JA4 problem to solve at this layer.
curl-cffi solves a different problem entirely: "I want to skip the browser, hit the JSON endpoint directly, and still have a TLS handshake that looks like a browser." That is a real and useful problem, but it is not the same problem.
Most "ja4 bypass" guides flatten this distinction and lead readers down the wrong fix path. This post separates them.
JA3 to JA4: What Actually Changed
JA3, originally published by Salesforce in 2017, hashed five fields from the TLS Client Hello: the SSL version, accepted ciphers, list of extensions, elliptic curves, and elliptic curve point formats. Concatenate them, MD5, done. For most of a decade this was the standard TLS fingerprint.
JA3 had two structural problems that mattered by 2024:
-
It was order-sensitive in a way that produced churn. Chrome started randomizing TLS extension order under GREASE and the extension permutation experiment. Suddenly real Chrome had multiple JA3 hashes per day. Detection vendors either had to maintain a rolling allowlist or accept false positives.
-
It only looked at one protocol layer. JA3 said nothing about ALPN negotiation, HTTP/2 SETTINGS frames, or the per-connection signature algorithm list. A library that copied Chrome's cipher suite list verbatim could match Chrome's JA3 while being trivially distinguishable at the HTTP/2 layer.
JA4, released by FoxIO in late 2023, addressed both. It is actually a family of fingerprints: JA4 (TLS Client Hello), JA4S (server-side), JA4H (HTTP), JA4X (X.509 certificate), JA4L (latency), and a few others. The TLS one — JA4 proper — sorts the cipher and extension lists before hashing, which kills the GREASE/permutation churn. It also factors in ALPN, the count of ciphers and extensions, the TLS version negotiated, and the signature algorithm list.
The output looks like t13d1516h2_8daaf6152771_b186095e22b6 — three segments separated by underscores, with the prefix telling you the protocol (t for TCP, q for QUIC) and the version, and the two hashes covering ciphers and extensions respectively.
The practical difference: a JA4 hash is stable across a real Chrome's randomized extension orderings, but it changes if you swap out the underlying TLS library. That makes it considerably harder to fake from outside a real browser.
How Detection Vendors Actually Use JA4
Three patterns dominate in 2026 production traffic.
Cloudflare consumes JA4 as part of its bot score. The publicly visible signal is the cf-ja4 header in worker traffic and the JA4 fields exposed in the Bot Management ruleset. A JA4 that does not match any known browser baseline pushes the score toward "likely bot" territory but does not block on its own — it combines with other signals (IP reputation, behavioral metrics, the cluster of JS-side checks) to produce a final decision.
DataDome uses JA4 as a hard gate on certain account-level rules. The pattern reported in their own engineering posts is: a JA4 hash claiming to be Chrome, paired with HTTP/2 SETTINGS that do not match Chrome's, returns 403 immediately without further evaluation. This is the inconsistency-detection pattern — they are not blocking on the JA4 itself, they are blocking on the mismatch between the JA4 and the rest of the request.
Akamai Bot Manager documents JA3 and JA4 as scored inputs in its bot detection pipeline. The interesting detail in their setup is that they evaluate JA4 across the whole connection lifetime, not just the initial handshake — so a TLS resumption that lands on a different JA4 than the original session is a flag.
The common thread: nobody blocks purely on the JA4. They block on JA4 combined with another signal, where the combination tells them the stack is not what it claims to be.
The Common Misunderstanding: Playwright Does Not Have a JA4 Problem
The single most common misconception in anti-bot Discord servers and Stack Overflow answers is some version of: "I am using Playwright and getting blocked, so I need to fix my JA4."
Walk through what Playwright actually does:
- It downloads or uses a system Chromium binary.
- It launches that binary as a subprocess.
- It attaches over Chrome DevTools Protocol.
- The browser process makes its own network requests using its own networking stack — which is the same networking stack that ships in Chrome Stable.
The TLS handshake is performed by //net in the Chromium source tree, which delegates to BoringSSL. BoringSSL's cipher list, extension order, ALPN behavior, and HTTP/2 SETTINGS are the cipher list, extension order, ALPN behavior, and HTTP/2 SETTINGS of real Chrome. Of course they are — it is the same code.
If you capture a JA4 from Playwright-driven Chromium and compare it to a JA4 from real Chrome on the same OS, they match. There is no fingerprint to fake at this layer because the handshake was already done by real Chrome.
What Playwright does leak — and what people are usually actually being blocked on — is at the JavaScript layer: navigator.webdriver, the cluster of CDP-injected globals (__playwright__binding__, __pwInitScripts), the runtime artifacts that detection scripts read from inside the page. None of those are TLS-layer signals. They are JS-layer signals. Fixing them with a "ja4 spoofer" is fixing the wrong layer.
The right diagnostic is: confirm your JA4 actually does match Chrome before you spend time on it. The tools section below covers that.
When You Actually Do Need curl-cffi
curl-cffi is a Python binding to a patched libcurl that ships with browser-impersonation TLS profiles. The use case it is built for is genuinely different from Playwright's:
- You have identified the HTTP API endpoint behind a SPA's UI.
- You do not need to render the page, run its JavaScript, click buttons, or wait for XHR responses.
- You want to make direct HTTP calls and have the TLS handshake look like Chrome.
This is a valid stack. Many scraping jobs are pure data extraction where the JS rendering is overhead. requests won't work because its urllib3-based TLS handshake produces a urllib3 JA4 — not Chrome's. httpx has the same issue. aiohttp likewise. They are all built on Python's ssl module which uses OpenSSL with a very different cipher and extension layout than BoringSSL.
curl-cffi swaps the TLS layer for a fork of curl that BoringSSL was retrofitted into, and exposes flags like impersonate="chrome131" that select cipher, extension, and HTTP/2 SETTINGS profiles matching specific Chrome versions. The output JA4 matches the target version's JA4.
The trade-off: you are now responsible for everything the browser would have given you for free. JavaScript-rendered content. CSRF token harvesting. The handshake match is solved, but every JS-layer signal the target site reads (canvas, WebGL, Sec-CH-UA, the cluster of window properties) does not exist for you to send. If the site checks those, you fail differently than Playwright fails.
The Decision Matrix
| Your stack needs… | Right tool | Why |
|---|---|---|
| SPA rendering, click-through flows, dynamic JS | Playwright + a real Chromium fork | TLS is already correct; JS-layer leaks are the actual problem |
| Pure JSON API extraction, no rendering | curl-cffi (Python) or fingerprint-suite (Node) | Skip the browser, fake the handshake |
| Mixed — sometimes API, sometimes render | Playwright with selective page.request for API calls | Both go through the same Chromium TLS stack |
| You don't know yet which one it is | Probe first (see checklist below) | Don't pick before you've measured |
The decision is driven by what layer the target site is reading from. If they require running JS to issue the actual data request (CSRF tokens generated by JS, signed payloads, anti-bot tokens injected into the request), you need the browser. If the API is callable with a stable token from a login flow, you can skip the browser and just need TLS to look right.
HTTP/2 and the Next Frontier
JA4's HTTP component (JA4H) covers the HTTP request layer at a high level — methods, headers, cookies — but the deeper signal anti-bot vendors are moving toward is the HTTP/2 connection profile: the SETTINGS frame values sent in the connection preface, the WINDOW_UPDATE pattern, the priority signaling structure on stream creation, and the order of pseudo-headers (:method, :path, :scheme, :authority).
Chrome's HTTP/2 connection preface has a specific shape. The SETTINGS frame includes HEADER_TABLE_SIZE: 65536, INITIAL_WINDOW_SIZE: 6291456, MAX_HEADER_LIST_SIZE: 262144, in that order. The WINDOW_UPDATE frame on the connection sends an increment of 15663105. Most non-Chrome HTTP/2 clients send different values in different orders.
This is the next layer detection vendors are building rules around. JA4 covers the TLS handshake; what comes after the handshake — the HTTP/2 connection preface — is increasingly checked independently. Tools like Akamai's BMP and Imperva's Advanced Bot Protection already factor it in.
The implication for tool choice: Playwright (real Chromium) is correct here automatically. curl-cffi's impersonate profiles cover this for the supported Chrome versions, but only for those versions — using an impersonate="chrome120" profile in 2026 means you are signaling "I am Chrome 120," which is itself a flag now that 120 is not the current stable.
Diagnostic Checklist: What's Your Stack Actually Sending
Before deciding what to fix, confirm what your stack is actually sending. The tools that matter:
-
tlsfingerprint.io — The University of Michigan's TLS observatory. Make a request from your stack, look up the JA4. Compare against current Chrome's JA4 hash on the same date. If they match, your TLS is fine; the problem is elsewhere.
-
browserscan.net and browserleaks.com/tls — Both expose JA3, JA4, and HTTP/2 fingerprints from your connection. Use these to capture the fingerprint your stack actually presents to a target site.
-
scrapfly.io/web-scraping-tools/http2-fingerprint — Specifically dumps the HTTP/2 SETTINGS frame your client sent. This is the one that catches
requestsandhttpxusers when their TLS looks right but the HTTP/2 layer outs them. -
Your own packet capture.
tcpdump -i any -w out.pcap port 443followed by Wireshark with the JA4 dissector is the ground-truth path when the third-party tools disagree.
A useful diagnostic flow:
- Capture the JA4 from your stack against a controlled endpoint (one of the above).
- Capture the JA4 from real Chrome on the same OS.
- If they match: your TLS is not the problem; investigate JS-layer leaks (
navigator.webdriver, CDP globals, canvas/audio fingerprint, Sec-CH-UA consistency). - If they don't match: your stack is not Playwright + Chromium, or your Chromium is unusual (custom build, headless mode signaling differently,
--disable-featuresremoving something Chrome ships). Switch tools or re-check the build.
Quick FAQ
Q: What's the difference between JA3 and JA4?
JA3 hashes five fields from the TLS Client Hello, in the order they appeared on the wire. JA4 sorts the cipher and extension lists before hashing (so it survives Chrome's randomization), adds ALPN and signature algorithm fields, and is part of a family covering HTTP, certificates, and latency. JA4 is what current detection vendors are consuming; JA3 is still in older rulesets but is being phased out.
Q: Does Playwright leak ja3 or ja4?
Playwright drives a real Chromium binary. The TLS handshake is performed by Chromium's BoringSSL, so the JA3 and JA4 hashes match real Chrome on the same version and OS. There is no Playwright-specific TLS leak. The leaks are at the JavaScript and CDP layers — navigator.webdriver, framework-injected window properties, Function.prototype.toString shapes from runtime patches.
Q: Do I need curl-cffi if I use Playwright?
No, not for the same problem. curl-cffi solves "I want to skip the browser entirely and still have a Chrome-like TLS handshake from Python." Playwright solves "I want to drive a real browser." If you are using Playwright, your TLS is already correct. Use curl-cffi only if your workload is pure HTTP API extraction with no rendering needs.
Q: What about TLS resumption and session tickets?
Detection vendors that look at JA4 across the connection lifetime will flag a session that resumes with a different JA4 than its initial handshake. Real Chrome's behavior here is consistent across resumptions; libraries that rotate cipher lists between sessions to "look more random" are signaling automation precisely by being inconsistent.
Q: My JA4 matches Chrome but I'm still being blocked. What now?
You are at the JS layer or the IP layer. Run through navigator.webdriver, Object.keys(window).filter(k => k.includes('cdc_') || k.includes('playwright') || k.includes('puppeteer')), and the canvas / audio / WebGL fingerprint stack. Independently, check your IP reputation — datacenter IPs and overused residential pools fail with clean fingerprints all the time.
Bottom Line
JA4 is a real signal, and it is increasingly the gate detection vendors are using to discriminate against Python HTTP clients. But "Playwright failing on a Cloudflare site" is almost never a JA4 problem, because Playwright's TLS handshake is real Chrome's TLS handshake. Treating the symptom (running a "ja4 spoofer" in front of Playwright) wastes effort that should go into the actual leak — usually at the JavaScript or CDP layer.
The clean decision rule: if you need the browser, use the browser, and your TLS is correct for free. If you don't need the browser, use curl-cffi (or fingerprint-suite on Node) and accept that you've signed up to fake everything the browser would have provided. Mix the two only when you've measured what you're actually being blocked on.
For teams running automation against targets that consume JA4 alongside JS-layer fingerprint depth, BotCloud provides an engine-level Chromium build with the standard Playwright/Puppeteer attachment surface, where the TLS handshake is real Chrome's and the JS-layer leaks the patched-driver category has to chase do not exist in the first place.