How to Bypass Cloudflare Turnstile for Web Scraping (2026)

how to bypass cloudflare turnstile for web scraping (2026)

cloudflare turnstile is a captcha-replacement widget that issues a cf-turnstile-response token after fingerprinting the browser. you cannot solve it server-side with raw http. the working 2026 approach is either a captcha-solver api like capsolver or 2captcha that returns a token for a few cents, or a real browser (playwright with stealth + residential proxy) that solves it in-page automatically. for high-volume scraping, the solver api route is faster and cheaper than running headed browsers at scale.

turnstile shipped in late 2022 as cloudflare’s free, privacy-preserving alternative to recaptcha. in 2026 it’s everywhere. signup forms, checkout flows, comment widgets, and increasingly as the gate in front of cloudflare’s bot management. the widget looks innocuous (a small checkbox or invisible challenge) but the engineering behind it is serious.

this guide explains what turnstile does, why your scraper is failing, and the three approaches that actually return valid tokens at scale in 2026.

what cloudflare turnstile does

turnstile sits as a <div class="cf-turnstile" data-sitekey="..."> on the target page. when the page loads, it pulls in the turnstile script from challenges.cloudflare.com/turnstile/v0/api.js. that script:

  1. checks for “private access tokens” issued by apple, google, etc. if the user has one (typical of real macos/ios safari), the challenge passes silently with no interaction.
  2. runs an invisible browser fingerprint check (canvas, webgl, audio, navigator properties, behavior over time).
  3. if those fail, presents a managed challenge. visible widget, sometimes with a checkbox or interactive proof-of-work.
  4. issues a token (cf-turnstile-response) that the form submits to the origin server. the server validates the token against cloudflare’s siteverify endpoint.

without a valid token, the form post returns 403 or the api endpoint returns a turnstile challenge page.

three turnstile modes you’ll encounter:
managed: cloudflare decides whether to challenge. usually invisible.
non-interactive: always invisible, lower friction.
invisible: completely hidden from the user. challenge runs in the background.

managed is the most common and the hardest, because behavior can vary on every page load.

why your scraper fails

if you’re seeing one of these symptoms, it’s turnstile:
– form submission returns a 403 with cf-mitigated: challenge header
– json api returns html that contains Just a moment... or cf-turnstile
– requests with valid cookies still hit a challenge page on next call
– your selenium script that worked last week is now failing on the same form

three reasons standard scrapers fail.

first, the cf-turnstile-response token is required. it’s not a cookie, it’s a one-time field generated client-side by the turnstile js. you can’t fake it. you can’t reuse one. each form submit needs a fresh token tied to the page session.

second, the turnstile script is heavily fingerprinted. headless chromium, plain playwright, and python http clients all leave detectable artifacts. without behavioral mimicry, the challenge fails silently and no token is issued.

third, ip reputation matters. requests from datacenter ips get a harder challenge. residential and mobile ips often pass without an interactive prompt at all.

the three approaches that work

ranked by reliability and cost.

approach 1: captcha solver api

the cheapest and fastest path for scale. you send the site key, target url, and user agent to a solver api. the solver returns a valid token. you submit the token with your form post. you never run a browser yourself.

capsolver is currently the price leader for turnstile. 2captcha and anti-captcha both work too. for the broader landscape see the best captcha solving services guide.

import requests
import time

CAPSOLVER_KEY = "your-capsolver-key"
TARGET_URL = "https://example.com/login"
SITE_KEY = "0x4AAAAAAA..."  # extracted from the page's data-sitekey attribute

def solve_turnstile(site_key, page_url):
    create = requests.post("https://api.capsolver.com/createTask", json={
        "clientKey": CAPSOLVER_KEY,
        "task": {
            "type": "AntiTurnstileTaskProxyLess",
            "websiteURL": page_url,
            "websiteKey": site_key,
        },
    }).json()
    task_id = create["taskId"]

    for _ in range(60):
        time.sleep(2)
        result = requests.post("https://api.capsolver.com/getTaskResult", json={
            "clientKey": CAPSOLVER_KEY,
            "taskId": task_id,
        }).json()
        if result["status"] == "ready":
            return result["solution"]["token"]
    raise TimeoutError("turnstile solver timed out")

token = solve_turnstile(SITE_KEY, TARGET_URL)
print(f"got token: {token[:40]}...")

# now submit the form with the token
r = requests.post(
    "https://example.com/login",
    data={
        "email": "user@example.com",
        "password": "test123",
        "cf-turnstile-response": token,
    },
)
print(r.status_code)

cost: $0.0008-0.001 per solve at capsolver pricing in 2026. that’s roughly $1 per 1000 turnstile bypasses. faster than a headed browser, no browser footprint to maintain.

2captcha equivalent:

import requests
import time

API_KEY = "your-2captcha-key"

def solve_turnstile_2c(site_key, page_url):
    r = requests.post("http://2captcha.com/in.php", data={
        "key": API_KEY,
        "method": "turnstile",
        "sitekey": site_key,
        "pageurl": page_url,
        "json": 1,
    }).json()
    captcha_id = r["request"]
    for _ in range(60):
        time.sleep(3)
        check = requests.get(
            f"http://2captcha.com/res.php?key={API_KEY}&action=get&id={captcha_id}&json=1"
        ).json()
        if check["status"] == 1:
            return check["request"]
    raise TimeoutError()

2captcha pricing in 2026 sits around $1.45 per 1000 turnstile solves. capsolver is consistently a few percent cheaper. at low volume the difference is rounding error.

approach 2: real browser with stealth

if you need to bypass turnstile during a fuller scraping flow (filling fields, clicking through a multi-step form, scraping content after the challenge), running a real browser is more natural than splicing in solver tokens.

import asyncio
from playwright.async_api import async_playwright

PROXY = {
    "server": "http://residential.example.com:8080",
    "username": "user",
    "password": "pass",
}

STEALTH = """
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
window.chrome = { runtime: {} };
Object.defineProperty(navigator, 'plugins', {get: () => [1,2,3,4,5]});
Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']});
"""

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=False,
            proxy=PROXY,
            args=["--disable-blink-features=AutomationControlled"],
        )
        ctx = await browser.new_context(
            viewport={"width": 1366, "height": 768},
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
            locale="en-US",
        )
        await ctx.add_init_script(STEALTH)
        page = await ctx.new_page()
        await page.goto("https://example.com/protected-page", wait_until="networkidle")

        # wait for turnstile to either pass silently or render the widget
        try:
            await page.wait_for_function(
                "() => document.querySelector('input[name=cf-turnstile-response]') && document.querySelector('input[name=cf-turnstile-response]').value !== ''",
                timeout=20000,
            )
            token = await page.evaluate(
                "() => document.querySelector('input[name=cf-turnstile-response]').value"
            )
            print(f"turnstile passed automatically. token: {token[:40]}...")
        except Exception:
            print("turnstile did not auto-pass, may need a click or solver fallback")

        # do whatever you came for
        html = await page.content()
        await browser.close()
        print(html[:2000])

asyncio.run(main())

key choices:
headless=False (or use xvfb for server). headless mode fails turnstile much more often than headed.
– residential or mobile proxies. datacenter ips push turnstile into harder challenges.
– the stealth init script patches obvious bot-tells.
– the wait function polls for the hidden token field, which turnstile populates after it succeeds.

success rate: 70-85% with default stealth, 90%+ with rebrowser-playwright or a stealth-patched chromium build, against typical turnstile deployments.

approach 3: managed scraping api

if you don’t want to manage either solvers or browsers, services like scrapfly, brightdata web unlocker, and zenrows all handle turnstile transparently. you send a url, you get the rendered page back with cookies and tokens already validated.

from scrapfly import ScrapflyClient, ScrapeConfig

client = ScrapflyClient(key="your-scrapfly-key")
result = client.scrape(ScrapeConfig(
    url="https://example.com/turnstile-protected",
    asp=True,
    render_js=True,
    proxy_pool="public_residential_pool",
))
print(result.content)

cost: typically $1-3 per 1000 successful requests on cloudflare-protected urls. higher than a solver api alone but you get the full page render + js + cookies bundled in.

extracting the site key

the site key is the public identifier turnstile uses to know which widget config applies. you need it for solver api calls.

it’s hardcoded in the page html. open dev tools, search for data-sitekey= or cf-turnstile. the value you want looks like 0x4AAAAAAA.... it’s safe to hardcode in your scraper because it’s public.

programmatic extraction:

import re
import requests

html = requests.get("https://example.com/login").text
match = re.search(r'data-sitekey="([^"]+)"', html)
if match:
    site_key = match.group(1)
    print(site_key)

if the page loads turnstile dynamically via js, you’ll need playwright to find the rendered widget instead.

proxy choice for turnstile

proxy typeturnstile pass ratetypical cost per gb
datacenter30-50%$0.50-2
shared residential60-75%$4-8
premium residential (bright data, oxylabs)80-90%$6-15
mobile (4g/5g)90-95%$10-30

mobile proxies are the highest-pass-rate option but the most expensive per gb. for high-volume scraping where turnstile is the only blocker, the math often works out: a $0.001 solver call beats a $0.05 mobile-proxy page load. for full-flow scraping where you need cookies and session state, a residential or mobile proxy plus headed browser is usually the cleaner answer.

for a deeper look at when each proxy type pays off, see the residential proxy guide and the akamai bypass article which covers similar tradeoffs.

hybrid approach: best of both

production scrapers usually combine the two cheaper options. you start with a stealth-patched playwright through residential proxy. if turnstile auto-passes, you keep going. if it doesn’t, you call the solver api as a fallback and inject the token.

import asyncio
import requests
import time
from playwright.async_api import async_playwright

CAPSOLVER_KEY = "your-key"

async def get_token_from_page(page):
    try:
        await page.wait_for_function(
            "() => document.querySelector('input[name=cf-turnstile-response]')?.value",
            timeout=12000,
        )
        return await page.evaluate(
            "() => document.querySelector('input[name=cf-turnstile-response]').value"
        )
    except Exception:
        return None

def solve_via_api(site_key, page_url):
    create = requests.post("https://api.capsolver.com/createTask", json={
        "clientKey": CAPSOLVER_KEY,
        "task": {"type": "AntiTurnstileTaskProxyLess",
                 "websiteURL": page_url, "websiteKey": site_key},
    }).json()
    tid = create["taskId"]
    for _ in range(60):
        time.sleep(2)
        r = requests.post("https://api.capsolver.com/getTaskResult", json={
            "clientKey": CAPSOLVER_KEY, "taskId": tid,
        }).json()
        if r["status"] == "ready":
            return r["solution"]["token"]
    return None

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        page = await browser.new_page()
        await page.goto("https://example.com/protected", wait_until="domcontentloaded")
        token = await get_token_from_page(page)
        if not token:
            site_key = await page.evaluate(
                "() => document.querySelector('.cf-turnstile').dataset.sitekey"
            )
            token = solve_via_api(site_key, page.url)
            await page.evaluate(f"""
                document.querySelector('input[name=cf-turnstile-response]').value = '{token}';
            """)
        print(f"have token: {token[:40]}")
        await browser.close()

asyncio.run(main())

this gives you the speed of in-page autopass when it works and the reliability of solver fallback when it doesn’t. about $0.0002-0.001 per page on average, which beats either pure approach.

what doesn’t work in 2026

a few approaches that show up in older blog posts but are dead now:

  • selenium with default chromedriver: navigator.webdriver detected instantly. fails 95%+ of turnstile challenges.
  • requests plus copied cookies: turnstile tokens are single-use. cookies don’t carry between scrape sessions reliably.
  • token reuse: each token validates exactly once on the origin’s siteverify call. reuse returns 401.
  • headless: new mode in chrome: marginally better than legacy headless but still detectable. not enough on its own.
  • vpn-only setups (no proxy): most consumer vpns are flagged. residential proxies are different beasts.

faq

is bypassing cloudflare turnstile illegal?
not in itself. scraping public data is legal in most jurisdictions. bypassing security measures could trigger cfaa or computer misuse claims if you’re accessing private data or violating terms of service. the web scraping legal guide covers the case law.

how much does it cost to solve turnstile?
solver apis charge $0.0008-0.0015 per token in 2026. for 1000 solves, expect $0.80-1.50 at capsolver, $1.40-1.50 at 2captcha.

can i bypass turnstile with python requests alone?
no. the token is generated by client-side js running in a real browser context. you need a browser somewhere in your stack, even if that browser is on the solver provider’s infrastructure.

why does my playwright script work locally but fail on a server?
servers usually run headless. headless chromium is more detectable. run with xvfb to fake a display: xvfb-run -a python script.py.

does undetected-chromedriver bypass turnstile?
sometimes, with residential proxy. success rate around 60-70% on managed-mode turnstile. rebrowser-playwright is more reliable in 2026.

what’s the difference between turnstile and recaptcha v3?
both are invisible-by-default scoring systems. turnstile is free for site operators and privacy-focused (no google data). recaptcha v3 is free for low volume but tied to google. solver pricing is similar, in the $0.001-0.002 range per token.

conclusion

cloudflare turnstile is solved territory in 2026. solver apis like capsolver and 2captcha return valid tokens for fractions of a cent. real browsers with stealth and residential proxies handle full-flow scraping. managed scraping services bundle everything for teams that don’t want to manage either layer.

start with the cheapest approach that fits your use case. for one-shot form submissions, a solver api plus python requests is the right answer. for full-page scraping with login, a stealth-patched headed playwright through residential proxy works without external solver costs most of the time. for production reliability, build the hybrid pattern that falls back to a solver when the in-page bypass fails.

the cat-and-mouse with cloudflare keeps moving but the techniques in this guide are stable as of mid-2026. revisit your stack every six months.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top
message me on telegram

Resources

Proxy Signals Podcast
Operator-level insights on mobile proxies and access infrastructure.

Multi-Account Proxies: Setup, Types, Tools & Mistakes (2026)