Anti-Bot

How Cloudflare Bot Management detects scraping in 2026

Cloudflare published how most of this works. Developer docs, blog posts, patent filings. But nobody's assembled the full picture of what actually fires when Bot Management scores your traffic. Here's every detection signal, what it inspects, and how scraping teams clear each one.

Hire a Clawd is our own service.

Cloudflare publishes most of how their bot detection works. Developer docs, blog posts, patent filings. Nobody reads them (or they read the marketing summary and skip the mechanism underneath). So here's what Bot Management actually inspects, signal by signal, pulled from the public documentation and confirmed against what we see when scrapes fail.

The frame here is public data at a reasonable pace. Not credential theft, not hammering logins.

JA4 TLS fingerprinting

Before your scraper sends a single HTTP header, it's already been fingerprinted.

The TLS ClientHello that opens the connection has a structure: cipher suites, extensions, supported groups, signature algorithms, and the order they appear in. Cloudflare hashes this into a JA4 fingerprint. (JA3 is dead. Chrome killed it by randomizing extension order starting in late 2023, so Cloudflare and everyone else moved to JA4 and normalized variants that account for the randomization.)

JA4 is computed at the edge, from the handshake Cloudflare terminates. You can't spoof it with a header. You can't lie about it. The handshake is the handshake.

Here's the problem: Python's requests library uses urllib3, which uses OpenSSL, which produces a TLS handshake that looks nothing like any browser's. Same story for Go's net/http, Node's https module, and basically every HTTP library that doesn't specifically impersonate a browser. Cloudflare knows what Chrome 126's ClientHello looks like. They know what Firefox 128's looks like. And they know what "this is obviously a script" looks like.

# Check your own JA4 fingerprint against a browser's
curl -s https://tls.peet.ws/api/all | python3 -c "
import sys, json
d = json.load(sys.stdin)
print('JA4:', d.get('tls', {}).get('ja4', 'N/A'))
print('JA3:', d.get('tls', {}).get('ja3_hash', 'N/A'))
"

Run that, then open the same URL in Chrome. Compare the output. If they don't match (and with stock curl, they won't), Cloudflare has a signal against you before your first byte of HTTP even arrives.

HTTP/2 frame fingerprinting

This catches people who fix JA4 but forget the layer above it. HTTP/2 connections open with a SETTINGS frame, and the values in that frame differ between implementations. Chrome sends a specific set of SETTINGS parameters in a specific order with specific values. Firefox sends different ones. Python's httpx sends yet another set entirely.

Cloudflare also watches:

The combination creates an HTTP/2 fingerprint that's independent of JA4 but correlated with it. A real Chrome browser produces a JA4 that says "Chrome" AND an HTTP/2 fingerprint that says "Chrome." A scraper using curl_cffi with impersonate="chrome" gets both right, which is most of why it works. A scraper using raw httpx gets neither.

Fingerprint-matching alone won't save a bad IP

Cloudflare scores IP reputation before the TLS handshake even matters. A rotating residential proxy pool with clean, non-flagged IPs is the base layer that makes everything above it work. Without it, a perfect JA4 still loses.

Canvas and WebGL fingerprinting

Once JavaScript runs (and on any Bot Management-protected page, it runs), Cloudflare's challenge script asks your browser to draw things and reads back the result.

Canvas fingerprinting renders specific text and shapes to a <canvas> element, then calls toDataURL() to get the pixel data. Different GPU drivers, font rendering engines, and anti-aliasing implementations produce slightly different output. This isn't unique to you (millions of Chrome-on-Windows-with-Intel-UHD users produce the same hash), but it's another signal in the stack.

WebGL goes deeper. It queries WEBGL_debug_renderer_info for the GPU vendor and renderer strings, then runs shader operations that stress specific GPU features. The results get hashed and scored against known populations.

The detection isn't "is this canvas hash known?" It's "does this canvas output match what this browser, on this OS, with this GPU should produce?" A headless Chrome on a Linux server with no GPU that claims to be Chrome on Windows with an NVIDIA card fails this hard. The canvas says one thing, the claimed environment says another, and Cloudflare scores the mismatch.

This is where most stealth plugins die.

Cloudflare's scripts probe the browser environment looking for contradictions. Some checks are well-documented, others we've reverse-engineered by watching what trips a challenge:

// Simplified version of what Cloudflare probes (real script is obfuscated)
const isAutomated = (() => {
  try {
    const desc = Object.getOwnPropertyDescriptor(navigator, 'webdriver');
    if (desc && desc.get) return true;  // overridden getter = patched
    if (navigator.webdriver === true) return true;
    for (const key of Object.keys(window)) {
      if (key.match(/^cdc_/)) return true;  // Chromedriver artifact
    }
    return false;
  } catch (e) {
    return true;  // errors here are suspicious on their own
  }
})();

The real detection script changes frequently, but the pattern stays: probe, compare, score. Inject-based stealth tools like the old playwright-stealth patch these values from the outside, and they can be caught doing it. A browser that spoofs at the binary level (Camoufox, for example) is harder to detect because the values read back as native, not patched.

A browser that doesn't need stealth patches

Anti-detect browsers spoof fingerprint values at the engine level, so there's no injected JavaScript for Cloudflare's probes to catch. If you're running scrapes against Bot Management targets, this is the layer that separates "blocked in 10 requests" from "running for weeks."

ML scoring and behavioral heuristics

This is where Cloudflare's documentation gets vague on purpose. Bot Management feeds all of the signals above (plus IP reputation, ASN, geolocation, request rate, mouse movement telemetry, and historical behavior across their entire network) into a machine learning model that outputs a bot score from 1 to 99. Site operators set their own thresholds: challenge anything below 30, hard-block anything below 10.

What we know from testing:

This is the hardest layer to beat because it's opaque and it adapts. You can't inspect the model. You can only observe the outputs and adjust. And the vendors like it that way.

Five layers of detection is a lot to keep alive

TLS fingerprints, HTTP/2 frames, canvas hashes, navigator probes, and a scoring model that shifts without warning. Hire a Clawd is a personal AI agent that manages the entire stack, rotates around new detections, and delivers clean data while you do something else.

See plans from $49/mo →

How scrapers actually clear these hurdles

Five layers sounds like a lot, and it is. But they interact, which means you can pick the right tool for the specific detection surface you're hitting instead of throwing the heaviest option at everything.

Sites with passive fingerprinting only (no JS challenge)

Use curl_cffi with browser impersonation. It matches the JA4 and HTTP/2 fingerprint of a real browser, runs no JavaScript (so none of the JS checks apply), and it's fast. Pair with residential proxies because datacenter IPs fail reputation checks before fingerprinting ever matters. We cover picking a pool in the residential proxy guide.

from curl_cffi import requests

resp = requests.get(
    "https://example.com/api/data",
    impersonate="chrome",
    proxies={"https": "http://user:pass@resi-gate.example.com:8080"},
)
print(resp.status_code, resp.text[:200])

Sites with active JS challenges (Turnstile, managed challenge)

You need a real browser with source-level spoofing. Camoufox patches Firefox at the binary level, so canvas, WebGL, navigator properties, and plugin lists all read back as native values. The Firefox TLS fingerprint is also less scrutinized than Chromium's (Cloudflare built their detection primarily around Chrome, and they've historically been softer on Firefox).

from camoufox.sync_api import Camoufox

with Camoufox(
    headless="virtual",   # virtual display, trusted more than pure headless
    geoip=True,           # match tz/locale to proxy exit IP
    humanize=True,
    proxy={
        "server": "http://resi-gate.example.com:8080",
        "username": "user",
        "password": "pass",
    },
) as browser:
    page = browser.new_page()
    page.goto("https://example.com/products")
    page.wait_for_load_state("networkidle")

    cookies = page.context.cookies()
    clearance = [c for c in cookies if c["name"] == "cf_clearance"]
    print(f"Got clearance: {bool(clearance)}")

geoip=True is the detail people miss. It aligns the browser's timezone, locale, and geolocation to the proxy's exit IP. A residential IP in São Paulo paired with a browser reporting America/New_York is exactly the incoherence Cloudflare's model catches. For a full walkthrough of the bypass side, see the Cloudflare bypass guide.

The ML and behavioral layer

No single fix. You manage it.

The risk/reward math is straightforward. Every additional evasion layer costs compute, bandwidth, or money. A site on Cloudflare's free tier doesn't need any of this; curl_cffi with a decent proxy gets through. A site on Bot Management Enterprise with custom rules might need every layer in the stack. Match the effort to the target. The general bot-detection checklist covers the same idea across all vendors, not just Cloudflare.

Skip the setup

The Anti-Detect Scraping Starter Kit

A ready-to-run Camoufox + residential proxy Python template with humanized timing, geo matching, and a pre-flight checklist for every detection layer on this page. Configured and tested so your first Bot Management scrape doesn't end on request one.

$19 one-time

Get the kit →

FAQ

What's the difference between Cloudflare's free tier and Bot Management?

The free tier runs basic checks: IP reputation, a simple JavaScript challenge (the "checking your browser" page), and WAF rules. Bot Management is the paid enterprise product that adds JA4 fingerprinting, HTTP/2 analysis, ML scoring, and cross-network reputation. The free tier is beatable with curl_cffi and a residential proxy. Bot Management usually requires a real browser with source-level spoofing.

Does Cloudflare share detection signals across sites?

Yes. They process traffic for millions of domains and correlate behavior across them. If your fingerprint cluster hits dozens of Cloudflare-protected sites in a short window, your bot score drops everywhere. This is one reason a single shared proxy pool with heavy traffic degrades over time: other people's scrapes poison the IP reputation you're counting on.

Can I solve Turnstile with a CAPTCHA service instead of fixing fingerprints?

You can, but it's expensive at volume and it doesn't fix the underlying problem. Turnstile in managed mode often resolves invisibly when the rest of your stack is clean. Fix the IP, TLS, and browser layers first. Reserve CAPTCHA solvers for the challenges that still fire after everything else checks out. See the CAPTCHA services comparison for current pricing.

What JA4 hash should my scraper produce?

There isn't one "right" hash. JA4 varies by browser version, OS, and TLS library. The goal is consistency: your JA4 should match the browser your User-Agent claims to be. If you claim Chrome 126 on Windows, your JA4 needs to be Chrome 126 on Windows. curl_cffi and Camoufox handle this automatically. If you're building a custom stack, compare your fingerprint against tls.peet.ws using the same browser you're impersonating.