Cookies & analytics

We'd like to use analytics to understand how this site is used. Nothing loads or fires until you agree. See our privacy policy for the full list of processors.

webclaw
PricingDemo
2,403
Back to blog
April 30, 2026Updated September 8, 2026Massi

JA4 Fingerprints Decoded: Format, Bot Score, curl 403s

On this page

  • TLS fingerprint vs Cloudflare: quick answer
  • What a TLS fingerprint actually is
  • The handshake is scored before User-Agent
  • What every default client looks like
  • Why User-Agent rotation is theatre
  • The matrix of options
  • Using Webclaw for extraction
  • The trade-off you actually pay
  • Diagnosing a failed request
  • Frequently asked questions
  • What is the difference between JA3 and JA4?
  • Can I change my JA3 in Python requests?
  • Why does Chrome ship GREASE values?
  • Does User-Agent rotation help against Cloudflare?
  • What is the fastest way to scrape Cloudflare-protected pages?
  • Why do some libraries call themselves "Chrome impersonation"?
  • How often does Chrome's TLS fingerprint change?
  • Can Cloudflare detect "Chrome-impersonating" libraries?
  • What is Cloudflare's bot score made of?
  • How do I check a Webclaw request?

You wrote a scraper in Python. You set a real Chrome User-Agent. You added the same Accept-Language header your browser sends. You opened the URL in Chrome and it loaded fine. You ran your scraper and got a 403.

This post is about why. The short answer: Cloudflare scored your TLS handshake before it ever read your User-Agent. The cipher order, the extensions, the GREASE values, the HTTP/2 SETTINGS, all of those tell Cloudflare you are not Chrome. The User-Agent says "Chrome." The connection itself says "Python."

This is the layer below most scraping advice. For the wider playbook see the pillar on bypass Cloudflare bot protection. This post is the deeper version: what a TLS fingerprint actually is, why every default HTTP client has a unique one, and how to match a real browser without paying the cost of launching one.

TLS fingerprint vs Cloudflare: quick answer

If Chrome loads a page and curl, Python requests, Go net/http, or Node fetch gets a 403, the User-Agent is probably not the problem.

  1. Cloudflare sees the TLS ClientHello before it reads headers.
  2. JA3 and JA4 fingerprints expose the real client family.
  3. HTTP/2 SETTINGS and header wire order add another fingerprint.
  4. A fake Chrome User-Agent on a library TLS stack is an inconsistency, not a disguise.

The practical fix is a browser-grade HTTP client for the first fetch, then a browser fallback only when the page really needs JavaScript. For the full decision tree see bypass Cloudflare bot protection and Cloudflare error codes.

TLS and HTTP/2 fingerprint by client. Default libraries score as bots. Real Chrome and Chrome-fingerprinted Rust score as browsers.
TLS and HTTP/2 fingerprint by client. Default libraries score as bots. Real Chrome and Chrome-fingerprinted Rust score as browsers.

What a TLS fingerprint actually is

When a client connects to a server over HTTPS, the first thing it sends is a ClientHello message. The ClientHello announces, in order:

  • The TLS versions the client supports.
  • The cipher suites it can negotiate.
  • The extensions it understands (SNI, ALPN, EC point formats, signed certificate timestamps, and so on).
  • The elliptic curves and signature algorithms it accepts.
  • Optional GREASE values, dummy entries Chrome inserts to keep the protocol healthy.

All of those fields are observable to the server before any byte of HTTP is sent. Cloudflare sees them. So does every other CDN and WAF.

A JA3 fingerprint is a hash of the ClientHello fields concatenated in a fixed order. Two clients that build the ClientHello differently will produce different JA3 hashes. Python requests has a JA3 hash. Chrome 142 has a different JA3 hash. They are not the same string and they cannot be made the same by changing a header.

JA4 is the newer fingerprint family. It captures more of the handshake plus ALPN context, and is more stable across modern protocol behavior. Cloudflare uses JA4 in production. Their public JA4 Signals post says they analyze more than 15 million unique JA4 fingerprints per day.

Past the TLS layer, HTTP/2 has its own fingerprint surface:

  • The order of SETTINGS frame parameters.
  • The values used for INITIAL_WINDOW_SIZE, MAX_HEADER_LIST_SIZE, and so on.
  • The order of pseudo-headers (:method, :authority, :scheme, :path).
  • The order of regular headers on the wire.
  • Whether the client uses HEADERS PRIORITY frames.

Two HTTP/2 clients that "look the same" at the application level can still send completely different SETTINGS and header order. Cloudflare reads that too.

The handshake is scored before User-Agent

This is the part most scrapers get wrong. The diagnostic chain is roughly:

  1. Client opens TCP connection.
  2. Client sends TLS ClientHello.
  3. Cloudflare hashes the ClientHello (JA3, JA4) and compares to known browser fingerprints.
  4. TLS handshake completes.
  5. Client sends HTTP/2 SETTINGS, opens stream, sends HEADERS frame.
  6. Cloudflare hashes the HTTP/2 fingerprint and the header wire order.
  7. Only now does Cloudflare read the User-Agent header.
  8. Cross-check: does the User-Agent match the fingerprint?

If steps 3 or 6 already classified you as bot, step 7 is just confirmation. The User-Agent never had a chance.

That is why your "real Chrome user agent" did nothing. The decision was made three steps earlier.

What every default client looks like

Same URL, six different clients, six different fingerprints.

curl, Python requests, httpx. All built on top of OpenSSL or its variants. The cipher set is the OpenSSL default, not Chrome's order. No GREASE values. No Client Hints. The JA4 hash matches "generic library", not any browser. Some sites flag this immediately.

node-fetch, axios, undici. Built on Node's TLS stack. Distinct cipher order. HTTP/2 SETTINGS in Node order. The JA4 hash matches Node, not Chrome. UA spoofing is irrelevant because the handshake already lost.

Go net/http. Stable Go runtime fingerprint, very recognizable. Cloudflare even exposes a public detection tag for it. Spoofing the UA does nothing because Cloudflare flags Go traffic at the transport layer.

Real Chrome 142. BoringSSL ClientHello, real cipher order, GREASE inserted, ALPN h2, Client Hints attached. HTTP/2 SETTINGS in Chrome's order. This is the baseline that Cloudflare's bot management considers normal browser traffic.

Headless Chromium driven by Puppeteer. The TLS fingerprint is technically Chrome (BoringSSL), but Linux container Chromium plus the driving process often mangles the HTTP/2 SETTINGS or the header wire order. Result: a "Chrome-like" fingerprint that does not quite match real Chrome. Some sites pass it. Aggressive Cloudflare configs do not. See why Puppeteer stealth stopped working for the full story.

Browser-emulating HTTP clients. Some libraries provide browser-like handshake profiles. Profile support and target acceptance vary by version; matching a fingerprint alone does not establish access.

The pattern: every general-purpose HTTP library has a fingerprint that screams "library." Only clients explicitly built to match a browser produce a browser fingerprint.

Why User-Agent rotation is theatre

A lot of scraping tutorials still recommend "rotate your User-Agent through a list of real browsers." This was useful in 2018. It is mostly noise in 2026.

User-Agent rotation does fix one specific problem: Cloudflare WAF rules that match on the literal string python-requests/2.x or Go-http-client/1.1. Default library UAs trigger 1020 errors on a non-trivial fraction of sites. Changing the UA to a real Chrome string clears those rules.

It does not fix:

  • Mismatch between the UA you claim and the JA4 you produce.
  • HTTP/2 SETTINGS frames that do not match the claimed browser.
  • Header wire order that does not match the claimed browser.
  • Missing Client Hints that Chrome would always send.
  • Missing ALPN protocols that Chrome would always advertise.

Cloudflare's Detection IDs docs call out exactly this case: a request where the headers were sent in a different order than expected for the claimed browser. They built a detection ID for it.

If your stack lies at one layer and tells the truth at another, the inconsistency itself becomes the signal.

The matrix of options

These approaches have different rendering requirements. They have different cost, speed, and pass-rate trade-offs.

ApproachExecutes page JavaScript?What to check
Standard HTTP clientNoResponse content and access requirements
Browser-emulating HTTP clientNoSupported profiles and target-specific results
Browser rendererYesRender completion, missing content and resource use
HTTP with rendering fallbackWhen neededEscalation behavior and total latency

An HTTP client avoids browser execution work but cannot execute page JavaScript. Choose a rendering path based on the target content and measured results.

For comparing scraping APIs head to head, see Best web scraping APIs for LLMs.

Using Webclaw for extraction

Webclaw offers public-page extraction, managed rendering, and automatic retries for some blocked pages. These features do not guarantee access. Inspect returned content and handle errors before indexing a response.

Use the API reference for request fields and the scraping diagnostic checklist when a target fails. Managed and self-hosted deployments have different capabilities and configuration.

The trade-off you actually pay

An HTTP client can avoid browser startup and rendering work. The benefit depends on the target, network conditions, and whether a browser is already running.

For server-rendered pages (most blogs, most product pages, most documentation, most news, most listings), this is a non-issue. The HTML you fetch contains the content. You parse it and move on.

For client-rendered pages (some SPAs, some dashboards, pages that hydrate the real content from XHR after the shell loads), a fingerprinted fetch returns the shell. You see a hollow <div id="root"></div> and you are missing the content.

Two strategies work here:

  1. Inspect the page once in DevTools and find the underlying API. Most SPAs make their content visible through a JSON endpoint. Hit that endpoint directly with the fingerprinted client. You skip the rendering step entirely.
  2. Escalate to a real browser only when needed. Run the fingerprinted fetch first. If the response body is missing the marker you expect (a price field, an article body, a product title), then escalate.

Webclaw supports managed rendering for dynamic content. Test target pages and account for rendering failures and latency.

Diagnosing a failed request

Compare the response status and body with the content your application expects. A 403 indicates rejection but does not identify which signal caused it. A 200 can still contain a challenge or an empty application shell. Neither status alone proves extraction success.

Record the target, client version, rendering setting, timestamp, and returned fields. Change one setting at a time and compare actual page content; do not infer a fingerprint match from a successful request.

Frequently asked questions

What is the difference between JA3 and JA4?

JA3 is the older TLS fingerprint, a hash of the ClientHello field values. JA4 is the newer family that also captures HTTP/2 metadata and ALPN context. JA4 is more stable across modern protocol behavior and harder to fake because it covers more of the handshake. Cloudflare uses both but JA4 is the one mentioned in their newer bot management docs.

Can I change my JA3 in Python requests?

Not directly. requests uses OpenSSL via urllib3. The ClientHello is built by OpenSSL with its default cipher order, which produces a fixed JA3 hash. To get a different fingerprint you need a library that builds the ClientHello differently. curl-cffi and tls-client are the common Python options.

Why does Chrome ship GREASE values?

GREASE (Generate Random Extensions And Sustain Extensibility) is Chrome's mechanism to prevent middleboxes from ossifying around specific extension values. Chrome inserts dummy extension and cipher values that any compliant server should ignore. Real browsers send GREASE. Most default libraries do not. Cloudflare reads the presence of GREASE as a strong "real browser" signal.

Does User-Agent rotation help against Cloudflare?

Only against the simplest WAF rules that string-match python-requests or Go-http-client. Against bot management with JA4 scoring, UA rotation does nothing. The handshake is scored before the UA is read.

What is the fastest way to scrape Cloudflare-protected pages?

Measure representative pages with and without rendering. An HTTP client may be sufficient for server-rendered content; pages that need JavaScript require a renderer or an accessible data endpoint.

Why do some libraries call themselves "Chrome impersonation"?

Because they specifically build the ClientHello to match a recent Chrome version's exact byte sequence: cipher order, GREASE values, extensions, ALPN protocols, signature algorithms. They also match Chrome's HTTP/2 SETTINGS frame and header wire order. The result is a JA4 hash that matches Chrome rather than the underlying language runtime.

How often does Chrome's TLS fingerprint change?

Browser handshakes can change between releases. Check the client library's supported profiles and test compatibility instead of assuming a fixed update interval.

Can Cloudflare detect "Chrome-impersonating" libraries?

Sometimes. If the library matches the JA4 but does not match the HTTP/2 SETTINGS or the header order, the inconsistency itself is detectable. The libraries that win are the ones that match every layer. The libraries that get caught are the ones that match TLS only and ignore HTTP/2.

What is Cloudflare's bot score made of?

Per Cloudflare's public Bot detection engines docs, the score combines a heuristics engine (request fingerprint matches), JavaScript Detections (browser environment checks), machine learning over header and session features, and a __cf_bm cookie that smooths the score across a request pattern. JA4 is one input among several. A coherent client is one that matches across all of them, not just the fingerprint.

How do I check a Webclaw request?

Use documented CLI or API output to inspect the response and verify extracted fields against the source. The current CLI does not document a JA4-reporting command.


Read next: Bypass Cloudflare bot protection | Cloudflare error codes for scrapers | Why Puppeteer stealth stopped working

Start building

Turn pages into clean agent context.

Cancel anytime. Use the dashboard, API, CLI, or MCP server from the same account.

Read the docs

Get started

Ship your agent today. Scrape forever.

Cancel anytime. Migrate from Firecrawl in 60 seconds with the compatibility layer.

Read the docs
webclaw

Clean, structured web data for LLMs and agents. Open source, built in Rust — run it on our cloud or your own hardware.

Book a call

Product

  • Cloud API
  • CLI Tool
  • MCP Server
  • Pricing

Free tools

  • All free tools
  • Website to Markdown
  • Website Table to CSV
  • Pricing Page Extractor
  • Brand Kit Extractor

Developers

  • Documentation
  • API Reference
  • SDKs
  • Changelog

Resources

  • Startup Dataset
  • Compare
  • Self-hosting
  • For OSS

Company

  • Blog
  • About
  • Sponsor
  • Affiliate
  • Contact
webclaw
webclaw
© 2026 webclaw · AGPL-3.0 · Built in RustAll systems operational
PrivacyTerms

Webclaw · Valerio Massimiani · P.IVA 17669361002 · Rome, Italy