Cloudflare Bot Protection#

Understand exactly how Cloudflare decides you’re a bot β€” then use the legitimate ways through, and know what it costs to fetch a page anyway.

⏱ ~10 min read Β· ~15 min hands-on πŸ”— needs: Anti-bot Patterns Β· Hidden JSON APIs Β· Legal & Ethical Scraping

Cloudflare sits in front of a large share of the web. A plain httpx or requests call often gets a 403 or an endless “checking your browser” loop, while Chrome loads the same page instantly. That gap isn’t magic β€” it’s four specific signals. Once you can name them, you know your options.

βš–οΈ This page teaches how detection works so you can access data you are allowed to β€” your own sites, sandboxes, or targets you have permission for. Getting past a bot wall does not grant permission; Terms and law still decide. See Legal & Ethical Scraping.

Try it in 5 minutes β€” see the fingerprint gap#

The first thing Cloudflare inspects is your TLS handshake, before a single header is read. tls.peet.ws echoes back the fingerprint it sees, so you can compare a Python client with a browser-impersonating one:

# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx>=0.28", "curl_cffi>=0.7"]
# ///
"""See why Cloudflare can tell a Python client from a browser β€” by its TLS fingerprint.

Run:  uv run tls_gap.py
"""

import httpx
from curl_cffi import requests as cffi

ECHO = "https://tls.peet.ws/api/all"  # echoes back the TLS fingerprint it sees


def show(label: str, get) -> None:
    tls = get(ECHO).json()["tls"]
    print(f"{label:20} JA3={tls['ja3_hash']}  JA4={tls['ja4']}")


show("httpx (Python)", lambda u: httpx.get(u, timeout=10))
show("curl_cffi(chrome)", lambda u: cffi.get(u, impersonate="chrome", timeout=10))

βœ… Two different JA3 hashes. Cloudflare sees the same difference β€” and the bare-Python fingerprint is the one on its watch-list. You changed nothing about your headers; the TLS handshake alone gave you away.

Why Cloudflare flags you β€” four signals#

flowchart TD
    R["Incoming request"] --> TLS{"TLS JA3/JA4 looks<br/>like a real browser?"}
    TLS -->|No| BLOCK["Block / challenge"]
    TLS -->|Yes| H2{"HTTP/2 fingerprint<br/>consistent with that browser?"}
    H2 -->|No| BLOCK
    H2 -->|Yes| SCORE{"Bot score 1–99"}
    SCORE -->|"low Β· 1–29"| CHAL["Managed Challenge<br/>/ Turnstile"]
    SCORE -->|"high"| ALLOW["Allow"]
    CHAL -->|"runs JS, returns token"| ALLOW
    CHAL -->|"fails"| BLOCK
SignalWhat it checksWhy a bare Python client fails
TLS JA3 / JA4Cipher + extension order in the TLS ClientHelloPython’s OpenSSL stack hashes differently from Chrome’s BoringSSL
HTTP/2 fingerprintSETTINGS frame, header order, priorityhttpx/requests send a different HTTP/2 profile than a browser
JS / Turnstile challengeRuns JavaScript, probes the browser runtime, issues a cf_clearance tokenA bare HTTP client has no JS engine β€” it can’t complete the challenge
Bot score (1–99)ML over IP, ASN, and behaviour; low = botA datacenter IP with no history scores low before you do anything

Cloudflare’s own docs describe the bot score (1 = certainly automated, 99 = certainly human) and JA3/JA4 fingerprinting.

The legitimate ways through β€” try these first#

Before you spend a day matching fingerprints, spend five minutes looking for a door that’s already open:

  1. Official API, data feed, or sitemap. Faster and stable. See Sitemaps, RSS & Structured Data.
  2. Public archives. Wayback Machine & Common Crawl already have the page β€” and Cloudflare never sees you.
  3. Ask. Email the operator for an API key or permission. Many say yes.
  4. You own the site? Allow-list your own crawler in the Cloudflare dashboard. (Running Cloudflare on your API is Week 7.)

When you must fetch it yourself β€” matching a real browser#

For a target you’re permitted to access, you close the gaps in order of effort:

  • Passive fingerprints β†’ curl_cffi. Impersonates a browser’s TLS (JA3/JA4) and HTTP/2 profile with no browser at all β€” fast, and enough when there’s no JS challenge. (github.com/lexiforest/curl_cffi)
  • Active JS / Turnstile β†’ a stealth browser. When a challenge must actually run JavaScript, drive a real browser patched to hide automation: Patchright (drop-in Playwright) or Camoufox (anti-detect Firefox).
  • Then reuse the token. Solve the challenge once in the browser, grab the cf_clearance cookie, and make fast follow-up requests with curl_cffi β€” but see the binding gotcha below.

When it fails#

SymptomCauseFix
403 even with perfect headersTLS/HTTP2 fingerprint says “Python”Use curl_cffi with impersonate=
Stuck in a “checking your browser” loopAn interactive JS/Turnstile challengeTLS impersonation isn’t enough β€” use Patchright/Camoufox
Worked once, then blockedcf_clearance is bound to your IP + User-AgentReuse the same IP and UA for the harvested cookie
Blocked instantly from a serverDatacenter ASN scores poorlyA clean residential/ISP IP scores far higher than AWS/GCP ranges
Turnstile widget never resolves headlessNeeds a real browser runtimeRun non-headless, or a stealth browser build

Your turn (β‰ˆ15 min)#

  1. Run tls_gap.py and record the two JA3 hashes. In one sentence, explain which one Cloudflare blocks and why.
  2. Add a third line that impersonates a different browser (impersonate="safari"), and confirm the fingerprint changes again.
  3. Visit Cloudflare’s public Turnstile demo and watch a managed challenge run in your own browser β€” that JS is exactly what a bare HTTP client can’t do.
  4. Defender’s view (optional): skim how you’d put this protection in front of your own API β€” Week 7 β†’ Cloudflare (defender’s side).

Checklist#

  • I can name the four signals Cloudflare uses: TLS JA3/JA4, HTTP/2 fingerprint, JS/Turnstile, bot score.
  • I can explain why a 403 can happen before my headers are even read.
  • I know when TLS impersonation (curl_cffi) is enough and when I need a stealth browser.
  • I know cf_clearance is bound to IP + User-Agent, and that datacenter IPs score badly.
  • I check for an API, feed, or archive before trying to match a browser.

Go deeper#

Playwright Web Scraping Tutorial β€” Become 100% Undetectable! β€” Thomas Janssen