Cloudflare Turnstile CAPTCHA: How to Solve and Bypass

Cloudflare Turnstile CAPTCHA: How to Solve and Bypass

Cloudflare Turnstile is Cloudflare’s CAPTCHA replacement, designed to verify human visitors without the friction of traditional CAPTCHAs. For regular users, it’s nearly invisible. For web scrapers, it’s a significant obstacle because it combines browser environment checks, behavioral analysis, and cryptographic challenges into a single verification step.

This guide covers how Turnstile works under the hood and the most effective methods to handle it in your scraping workflows.

What Is Cloudflare Turnstile?

Turnstile launched in 2022 as a free alternative to reCAPTCHA and hCAPTCHA. Unlike traditional CAPTCHAs that require users to solve visual puzzles, Turnstile runs a series of invisible browser challenges that verify the visitor is a real human using a real browser.

How Turnstile Differs from Traditional CAPTCHAs

FeaturereCAPTCHA v2hCaptchaTurnstile
Visual puzzleYesYesRarely
Browser checksBasicModerateExtensive
PrivacyLow (Google tracking)MediumHigh
User frictionHighHighVery Low
Bot difficultyMediumMediumHigh

Turnstile’s Verification Process

When a page loads with Turnstile, the following happens:

  1. Widget loads: A JavaScript file from challenges.cloudflare.com is loaded
  2. Environment profiling: Turnstile checks the browser environment (canvas, WebGL, audio context, etc.)
  3. Behavioral analysis: Mouse movements, timing patterns, and interaction signals are analyzed
  4. Proof of Work: The browser performs a lightweight computational challenge
  5. Token generation: If all checks pass, a cf-turnstile-response token is generated
  6. Server verification: The token is sent to the server, which validates it with Cloudflare’s API

The critical piece for scrapers is the cf-turnstile-response token. This token must be included in form submissions or API requests for the server to accept them.

Identifying Turnstile on a Page

Turnstile widgets are embedded via a specific HTML pattern:

<!-- Turnstile widget -->
<div class="cf-turnstile"
     data-sitekey="0x4AAAAAAXXXXXXXXXXXXXXXXX"
     data-callback="onTurnstileSuccess">
</div>

<!-- Or explicit rendering -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

You can detect it programmatically:

import requests
from bs4 import BeautifulSoup

response = requests.get("https://target-site.com")
soup = BeautifulSoup(response.text, 'html.parser')

# Check for Turnstile widget
turnstile_div = soup.find('div', class_='cf-turnstile')
if turnstile_div:
    sitekey = turnstile_div.get('data-sitekey')
    print(f"Turnstile detected! Sitekey: {sitekey}")

# Check for Turnstile script
turnstile_scripts = soup.find_all('script', src=lambda s: s and 'challenges.cloudflare.com/turnstile' in s)
if turnstile_scripts:
    print("Turnstile script found")

Method 1: Browser Automation

The most reliable way to solve Turnstile is with a real browser. Since Turnstile checks the browser environment, a properly configured headless browser can pass the challenge naturally.

Using Undetected ChromeDriver

import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

def solve_turnstile_with_uc():
    options = uc.ChromeOptions()
    options.add_argument("--window-size=1920,1080")

    driver = uc.Chrome(options=options)

    try:
        driver.get("https://target-site.com/login")

        # Wait for Turnstile to load and solve
        time.sleep(5)

        # Check if Turnstile iframe is present
        iframes = driver.find_elements(By.CSS_SELECTOR, 'iframe[src*="challenges.cloudflare.com"]')

        if iframes:
            print("Turnstile iframe detected, waiting for auto-solve...")

            # Turnstile usually solves within 2-5 seconds for real browsers
            time.sleep(8)

            # Extract the turnstile response token
            token = driver.execute_script(
                "return document.querySelector('[name=\"cf-turnstile-response\"]')?.value"
            )

            if token:
                print(f"Turnstile token obtained: {token[:50]}...")
                return token
            else:
                print("Token not found. Turnstile may not have been solved.")

        return None

    finally:
        driver.quit()

token = solve_turnstile_with_uc()

Using Playwright

const { chromium } = require('playwright');

async function solveTurnstile() {
    const browser = await chromium.launch({
        headless: false,  // Turnstile checks for headless mode
        args: ['--window-size=1920,1080']
    });

    const context = await browser.newContext({
        userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
        viewport: { width: 1920, height: 1080 }
    });

    const page = await context.newPage();
    await page.goto('https://target-site.com/login');

    // Wait for Turnstile to appear and resolve
    try {
        await page.waitForFunction(() => {
            const input = document.querySelector('[name="cf-turnstile-response"]');
            return input && input.value.length > 0;
        }, { timeout: 30000 });

        const token = await page.evaluate(() => {
            return document.querySelector('[name="cf-turnstile-response"]').value;
        });

        console.log(`Token: ${token.substring(0, 50)}...`);
        return token;
    } catch (e) {
        console.log('Turnstile did not resolve in time');
        return null;
    } finally {
        await browser.close();
    }
}

For deeper stealth configurations, see our Playwright Stealth guide and Puppeteer Stealth guide.

Method 2: CAPTCHA Solving Services

CAPTCHA solving services can solve Turnstile challenges, either through human solvers or specialized AI systems.

How It Works

  1. You send the sitekey and page URL to the service
  2. The service solves the challenge (either with a real browser or human worker)
  3. You receive the cf-turnstile-response token
  4. You include the token in your request to the target site

Example with a Generic CAPTCHA Service API

import requests
import time

def solve_turnstile_with_service(sitekey, page_url, api_key):
    # Step 1: Submit the challenge
    create_response = requests.post(
        "https://api.captchaservice.com/createTask",
        json={
            "clientKey": api_key,
            "task": {
                "type": "TurnstileTaskProxyless",
                "websiteURL": page_url,
                "websiteKey": sitekey,
            }
        }
    )

    task_id = create_response.json()["taskId"]
    print(f"Task created: {task_id}")

    # Step 2: Poll for result
    for _ in range(30):  # Max 30 attempts
        time.sleep(3)

        result_response = requests.post(
            "https://api.captchaservice.com/getTaskResult",
            json={
                "clientKey": api_key,
                "taskId": task_id
            }
        )

        result = result_response.json()

        if result["status"] == "ready":
            token = result["solution"]["token"]
            print(f"Solved! Token: {token[:50]}...")
            return token

        print(f"Status: {result['status']}, waiting...")

    return None

# Usage
sitekey = "0x4AAAAAAXXXXXXXXXXXXXXXXX"  # From the cf-turnstile div
page_url = "https://target-site.com/login"
api_key = "YOUR_API_KEY"

token = solve_turnstile_with_service(sitekey, page_url, api_key)

Using the Token

Once you have the token, include it in your form submission or API request:

# Method A: Form submission
form_data = {
    "username": "user@example.com",
    "password": "password123",
    "cf-turnstile-response": token
}

response = requests.post(
    "https://target-site.com/login",
    data=form_data,
    headers=headers
)

# Method B: JSON API
json_data = {
    "email": "user@example.com",
    "cf-turnstile-response": token
}

response = requests.post(
    "https://target-site.com/api/login",
    json=json_data,
    headers={**headers, "Content-Type": "application/json"}
)

For a comparison of CAPTCHA solving providers, see our CAPTCHA solving services review.

Method 3: Token Harvesting

Token harvesting involves solving Turnstile in a real browser and extracting the token for use in automated requests.

import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
import time
import queue
import threading

class TurnstileTokenHarvester:
    def __init__(self, target_url, num_browsers=3):
        self.target_url = target_url
        self.num_browsers = num_browsers
        self.token_queue = queue.Queue()
        self.running = True

    def harvest_loop(self, browser_id):
        options = uc.ChromeOptions()
        options.add_argument("--window-size=1920,1080")

        driver = uc.Chrome(options=options)

        while self.running:
            try:
                driver.get(self.target_url)
                time.sleep(6)

                token = driver.execute_script(
                    "return document.querySelector('[name=\"cf-turnstile-response\"]')?.value"
                )

                if token:
                    self.token_queue.put({
                        "token": token,
                        "timestamp": time.time(),
                        "browser": browser_id
                    })
                    print(f"Browser {browser_id}: Token harvested")

                # Refresh page for next token
                time.sleep(2)

            except Exception as e:
                print(f"Browser {browser_id} error: {e}")
                time.sleep(5)

        driver.quit()

    def start(self):
        threads = []
        for i in range(self.num_browsers):
            t = threading.Thread(target=self.harvest_loop, args=(i,))
            t.daemon = True
            t.start()
            threads.append(t)
        return threads

    def get_token(self, timeout=30):
        try:
            entry = self.token_queue.get(timeout=timeout)
            # Tokens expire, typically within 300 seconds
            age = time.time() - entry["timestamp"]
            if age > 280:
                return self.get_token(timeout)
            return entry["token"]
        except queue.Empty:
            return None

# Usage
harvester = TurnstileTokenHarvester("https://target-site.com/login")
harvester.start()

# Use harvested tokens in your scraper
for url in urls_to_scrape:
    token = harvester.get_token()
    if token:
        response = requests.post(url, data={"cf-turnstile-response": token})

Method 4: FlareSolverr for Turnstile Pages

FlareSolverr can handle Turnstile challenges since it uses a full browser internally:

import requests

def solve_turnstile_flaresolverr(url):
    payload = {
        "cmd": "request.get",
        "url": url,
        "maxTimeout": 60000
    }

    response = requests.post("http://localhost:8191/v1", json=payload)
    result = response.json()

    if result["status"] == "ok":
        cookies = result["solution"]["cookies"]
        user_agent = result["solution"]["userAgent"]
        html = result["solution"]["response"]

        return {
            "cookies": {c["name"]: c["value"] for c in cookies},
            "user_agent": user_agent,
            "html": html
        }

    return None

See our FlareSolverr guide for setup instructions.

Turnstile Modes and Difficulty Levels

Turnstile has three modes that site owners can configure:

Managed Mode

The default mode that decides whether to show an interactive challenge or pass invisibly. Most scraping scenarios encounter this mode.

Non-Interactive Mode

Always runs invisibly in the background. Easier for automation since no click is needed, but still requires a real browser environment.

Invisible Mode

Completely invisible to users. Similar to non-interactive but designed for sites that don’t want any visual widget. The cf-turnstile-response is still required server-side.

Tips for Reliable Turnstile Solving

1. Avoid Pure Headless Mode

Turnstile actively detects headless browsers. Use headed mode or configure your headless browser to pass headless detection tests:

options = uc.ChromeOptions()
# Don't use --headless flag
# Use virtual display instead for servers
# pip install pyvirtualdisplay
from pyvirtualdisplay import Display
display = Display(visible=0, size=(1920, 1080))
display.start()

2. Use Residential Proxies

Turnstile factors in IP reputation. Datacenter IPs receive extra scrutiny. Pair your browser automation with residential proxies for higher success rates.

options = uc.ChromeOptions()
options.add_argument("--proxy-server=http://user:pass@residential-proxy:port")

3. Simulate Human Behavior

Turnstile checks for mouse movements and interaction patterns. Add realistic mouse movement to your automation:

from selenium.webdriver.common.action_chains import ActionChains
import random

def simulate_human(driver):
    """Add realistic mouse movements before Turnstile check."""
    actions = ActionChains(driver)

    # Random mouse movements
    for _ in range(random.randint(3, 7)):
        x = random.randint(100, 800)
        y = random.randint(100, 600)
        actions.move_by_offset(x, y)
        actions.pause(random.uniform(0.1, 0.5))

    actions.perform()

4. Token Expiration

Turnstile tokens expire after approximately 300 seconds (5 minutes). Always use tokens immediately after obtaining them, and implement expiration checking in your token harvesting system.

Troubleshooting Common Issues

Token Obtained But Server Rejects It

The token may have expired, or the server validates additional parameters (like cookies or IP address). Ensure your automated request matches the environment where the token was generated.

Turnstile Loops (Challenge Keeps Reappearing)

This usually indicates failed browser environment checks. Try:

  • Using a newer Chrome version
  • Ensuring JavaScript execution environment is complete
  • Checking that WebGL and Canvas are not blocked

Inconsistent Success Rates

Turnstile’s difficulty adapts based on traffic patterns. What works at 95% today might drop to 50% tomorrow. Build monitoring and fallback mechanisms:

def fetch_with_fallback(url, sitekey):
    # Try browser automation first
    token = solve_turnstile_with_uc()
    if token:
        return submit_with_token(url, token)

    # Fallback to CAPTCHA service
    token = solve_turnstile_with_service(sitekey, url, api_key)
    if token:
        return submit_with_token(url, token)

    # Last resort: manual token harvesting
    return None

Conclusion

Cloudflare Turnstile is one of the more challenging anti-bot systems to handle because it combines environmental checks, behavioral analysis, and cryptographic challenges. Browser automation with stealth plugins remains the most reliable approach, though CAPTCHA solving services provide a useful fallback.

The key is to maintain a real browser environment, use residential proxies, and build redundancy into your solving pipeline. As Turnstile evolves, staying current with the latest browser automation tools is essential.

For related guides, see our Cloudflare bypass overview, browser fingerprinting guide, and how websites detect bots.


Related Reading

Scroll to Top