Most "how to bypass Cloudflare" tutorials on the internet stopped working in late 2024. The reason is not that Cloudflare added new defenses. It is that the old defenses, JA3 fingerprinting and basic header checks, were replaced with a new generation that breaks every shortcut. This guide explains what Cloudflare actually checks in 2026, why cloudscraper, undetected-chromedriver, and most "stealth" plugins fail, and the four-layer recipe that still works in production.
The honest summary up front: there is no single trick. Bypassing Cloudflare reliably requires the right IP, the right TLS fingerprint, the right HTTP/2 frame ordering, and (for Bot Management tier) a real browser. Get any one of these wrong and you fail the edge filter before your request ever reaches the origin server.
Cloudflare's bot detection runs as a pipeline of filters at the edge. Each request passes through them in order. The earliest filter to reject wins, which means a request can fail on IP reputation alone before any fingerprint or header is examined.
This is the first and cheapest check. Cloudflare maintains a database keyed by ASN (Autonomous System Number). ASNs belonging to well-known cloud providers (AWS AS16509, GCP AS15169, Azure AS8075, OVH AS16276, DigitalOcean AS14061, Hetzner AS24940, Vultr AS20473) get the highest scrutiny. Even with a perfect browser-grade fingerprint, a request originating from these ASNs is treated as guilty until proven innocent.
Residential ASNs (Comcast AS7922, Verizon AS701, China Telecom AS4134, BT AS2856) are presumed legitimate. A request from a residential IP with a clumsy Python fingerprint can still pass, while a request from a datacenter IP with a perfect Chrome fingerprint will be challenged or blocked.
JA4 is the 2023 successor to JA3, released by FoxIO. It hashes specific TLS ClientHello fields: TLS version, cipher suites (in order), extensions (in order), ALPN protocols, signature algorithms, and supported groups. The output is a deterministic fingerprint like t13d1516h2_8daaf6152771_b1ff8ab2d16f.
Real Chrome 124 produces one specific JA4. Real Firefox 124 produces a different one. Python's requests library (which uses urllib3 and OpenSSL) produces a JA4 that no real browser ever emits, because the cipher order and extension list match neither Chrome nor Firefox. Cloudflare flags this pattern before parsing any HTTP header.
The implication: every library that uses the system OpenSSL or stock Python TLS is detectable. This includes requests, aiohttp, httpx (default config), and any "bypass" wrapper built on top of them.
HTTP/2 connections carry settings frames, window updates, priority frames, and headers frames. Browsers send these in a specific order with specific values. Chrome 124 sends an initial SETTINGS frame with these exact values, then a WINDOW_UPDATE, then HEADERS. Python's httpx with HTTP/2 enabled sends a different settings payload and skips the priority frames Chrome includes.
The HTTP/2 pseudo-header order also leaks identity. Chrome sends :method, :authority, :scheme, :path in that order. Firefox sends :method, :path, :authority, :scheme. Most Python and Go libraries serialize them alphabetically. Cloudflare matches the order against the expected pattern for the declared User-Agent and flags mismatches.
HTTP/1.1 headers preserve insertion order. Real Chrome sends headers in a fixed order: Host, Connection, Cache-Control, sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, Upgrade-Insecure-Requests, User-Agent, Accept, Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-User, Sec-Fetch-Dest, Accept-Encoding, Accept-Language. Python's requests reorders them and sometimes capitalizes differently. Even setting headers manually in the right order does not help because the underlying library re-sorts them.
If the prior filters pass but the score is still suspicious, Cloudflare serves a JavaScript challenge. The script computes a token from browser environment values (canvas hash, WebGL renderer, audio context, navigator properties, timing measurements) and submits it via XHR. No HTTP client can solve this. You need a real browser, and the browser must look real (no navigator.webdriver = true, no missing properties).
On Bot Management tier, Cloudflare collects mouse movement, scroll velocity, click timing, and keystroke dynamics through a continuously running script. A headless browser that loads the page and immediately scrapes without ever moving the mouse is detected within seconds. Real users move erratically, pause, and re-read. Bots scroll linearly and click pixel-perfect.
The shortcut tools that powered 2021-2024 scraping all share one failure mode: they patch the visible surface but not the underlying TLS stack.
| Tool | What it patches | Why it fails in 2026 |
|---|---|---|
cloudscraper | JS challenge solver (legacy) | Cloudflare moved to Turnstile in 2023; legacy challenges deprecated |
undetected-chromedriver | Patches Selenium leak flags | JA4 still leaks from underlying chromedriver TLS; navigator.webdriver only one of 40+ signals |
selenium-stealth | Injects spoofed JS properties | Cloudflare reads from C++ layer, not JS; spoof detectable as inconsistent |
FlareSolverr | Wraps Chromium for one-off challenge solve | cf_clearance bound to solver IP; fresh proxy invalidates token immediately |
Stock puppeteer-extra-plugin-stealth | Patches ~17 detection points | Cloudflare added ~12 new checks in 2024-2025 not covered by the plugin |
The pattern is clear: tools that wrap a real browser and patch known leaks lose the arms race within months. Tools that simulate a browser's TLS stack (curl_cffi, tls-client) have stayed alive because they sit closer to the wire.
A bypass that works in 2026 needs all four layers. Skipping any one drops success rate to single digits.
This is non-negotiable for any site running Cloudflare Pro or above. The IP reputation filter rejects datacenter ASNs before the TLS handshake completes. Use a residential proxy with a sticky session for at least 10 minutes so the cf_clearance cookie survives.
Use curl_cffi (Python) or tls-client (Go/Python). Both link against a modified BoringSSL that ships with Chrome's exact cipher order, extension list, and ALPN values.
from curl_cffi import requests
response = requests.get(
"https://target.com/api/products",
impersonate="chrome124",
proxies={"https": "http://user-session-abc123:[email protected]:10001"},
timeout=30,
)
print(response.status_code, len(response.text))
The impersonate="chrome124" parameter swaps out the TLS stack so the ClientHello matches Chrome 124 byte-for-byte. The JA4 hash will be identical to a real Chrome 124 install. Pair it with a residential proxy and you pass the first two filters.
curl_cffi sets headers in Chrome's order automatically when you use impersonate. If you add custom headers, append them at the end rather than inserting in the middle. Avoid setting headers that real Chrome would not send (e.g., X-Requested-With on a normal navigation).
For Accept-Language, match it to the proxy's geographic location. A US residential IP sending Accept-Language: zh-CN,zh;q=0.9 is a small but real signal. Use en-US,en;q=0.9 for US IPs, de-DE,de;q=0.9 for German IPs, and so on. This is one of the few signals you can fix for free.
If the target triggers a Managed Challenge (you see a CF challenge page in the response body), curl_cffi alone cannot solve it. Switch to Playwright with patched Chromium.
from playwright.sync_api import sync_playwright
from rebrowser_playwright.sync_api import sync_playwright as rebrowser_sync
with rebrowser_sync() as p:
browser = p.chromium.launch(
headless=True,
proxy={
"server": "http://gate.jibaoproxy.com:10001",
"username": "user-session-abc123",
"password": "pass",
},
)
context = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
viewport={"width": 1920, "height": 1080},
locale="en-US",
timezone_id="America/New_York",
)
page = context.new_page()
page.goto("https://target.com/", wait_until="networkidle")
# cf_clearance is now in context.cookies()
cookies = context.cookies()
cf_clearance = next((c for c in cookies if c["name"] == "cf_clearance"), None)
browser.close()
Use rebrowser-playwright instead of stock playwright. It patches the CDP runtime leaks (Runtime.enable, exposure of CDP commands to the page) that Cloudflare reads to detect headless automation. After the challenge passes, extract cf_clearance from cookies and hand it back to curl_cffi for the actual scraping loop. The browser is needed once per session, not per request.
The instinct to use the cheapest proxy is almost always wrong on Cloudflare-protected sites. Walk through the math with real numbers.
Assumptions: average HTML page 500KB, scraping 100,000 pages total, target uses Cloudflare Pro with Bot Fight Mode enabled.
| Setup | Bandwidth cost | Success rate | Requests per success | Total cost | Cost per page |
|---|---|---|---|---|---|
| Datacenter $1/GB + requests | $0.0005 / req | 0.5% | 200 | $10,000 | $0.10 |
| Datacenter $1/GB + curl_cffi | $0.0005 / req | 3% | 33 | $1,650 | $0.0165 |
| Residential $6.8/GB + requests | $0.0033 / req | 40% | 2.5 | $825 | $0.0083 |
| Residential $6.8/GB + curl_cffi | $0.0033 / req | 92% | 1.09 | $360 | $0.0036 |
| Residential $6.8/GB + Playwright (rebrowser) | $0.017 / req (assets + JS) | 96% | 1.04 | $1,700 | $0.017 |
Two findings worth noting:
This is the pattern we recommend to customers who scrape Cloudflare-protected targets at scale. It minimizes Playwright usage (which is slow and expensive) and maximizes curl_cffi usage (which is fast and cheap).
from curl_cffi import requests as cf_requests
from rebrowser_playwright.sync_api import sync_playwright
PROXY_USER_FMT = "user-session-{sid}"
PROXY_HOST = "http://gate.jibaoproxy.com:10001"
PROXY_PASS = "your_password"
def get_cf_clearance(target_url, session_id):
"""One-shot Playwright call to solve the challenge and extract cf_clearance."""
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
proxy={"server": PROXY_HOST,
"username": PROXY_USER_FMT.format(sid=session_id),
"password": PROXY_PASS},
)
ctx = browser.new_context(user_agent="Mozilla/5.0 ... Chrome/124.0.0.0 Safari/537.36")
page = ctx.new_page()
page.goto(target_url, wait_until="networkidle", timeout=45000)
cookies = ctx.cookies()
browser.close()
return {c["name"]: c["value"] for c in cookies}
def scrape_with_clearance(urls, session_id, cookies):
"""Reuse the same session_id (sticky IP) for all requests."""
proxy = f"http://{PROXY_USER_FMT.format(sid=session_id)}:{PROXY_PASS}@gate.jibaoproxy.com:10001"
out = []
for url in urls:
r = cf_requests.get(
url,
impersonate="chrome124",
proxies={"https": proxy},
cookies=cookies,
timeout=30,
)
if r.status_code == 200:
out.append((url, r.text))
elif r.status_code in (403, 503) and "challenge" in r.text.lower():
# cf_clearance expired; re-solve
cookies = get_cf_clearance(url, session_id)
r = cf_requests.get(url, impersonate="chrome124",
proxies={"https": proxy}, cookies=cookies)
out.append((url, r.text))
return out, cookies
# Usage
session_id = "scrape-job-2026-05-27"
cookies = get_cf_clearance("https://target.com/", session_id)
results, cookies = scrape_with_clearance(target_urls, session_id, cookies)
Key points in this recipe:
Some Cloudflare deployments cannot be bypassed at any reasonable cost. If you see these signals, switch strategies (use an API, partner with the site owner, or buy the data) instead of burning budget on a bypass that will never stabilize.
| Target tier | Recipe | Expected success rate |
|---|---|---|
| CF Free / Pro (no Bot Fight) | Datacenter + curl_cffi | 60-75% |
| CF Free / Pro + Bot Fight Mode | Residential + curl_cffi | 85-93% |
| CF Business | Residential + curl_cffi + occasional Playwright | 80-90% |
| CF Business + Turnstile | Residential + rebrowser-playwright (every request) | 70-85% |
| CF Enterprise + Bot Management | Mobile residential + rebrowser + behavior simulation | 30-60% |
| CF Enterprise + Bot Management + custom WAF | Do not scrape; pursue API or partnership | <10% |
JIBAO Proxy offers dynamic residential proxies with 90M+ IPs across 240+ countries, sticky sessions up to 60 minutes, and country-level targeting starting at $6.8/GB. For mobile IPs, see dynamic mobile proxies. Both work out of the box with curl_cffi, tls-client, Playwright, Puppeteer, Selenium, and any HTTP client that supports HTTP/SOCKS5 proxies.
Related reading: Sticky vs Rotating Proxy Sessions covers session configuration in depth. Proxies for AI Agents explains the AI agent use case that overlaps heavily with the Cloudflare bypass workflow.
Get $5 free credit. Run the curl_cffi + sticky residential combo against your target before committing.
Start Free TrialNew users get $5 USDT instantly, plus an extra first-deposit reward — limited-time offer.