You’re scraping a site. Everything looks fine. Then Cloudflare Turnstile shows up and your script just stops.

It’s not a traditional CAPTCHA — there’s no image puzzle to solve. Turnstile works by checking your browser’s fingerprint, JavaScript signals, and behavior. And standard Playwright fails that check almost immediately.

Here’s how to get past it.


What is Cloudflare Turnstile?

Turnstile is Cloudflare’s bot detection widget. It replaced the old “I’m not a robot” checkbox in most places.

You’ll see it in two forms:

  • Turnstile Widget — a small checkbox embedded in a form. Sometimes it passes you automatically, sometimes you have to click it.
  • Cloudflare Interstitial — a full-page challenge that blocks access to the site until it completes. No checkbox visible, just a loading screen that either passes or doesn’t.

Both can be solved programmatically. The widget is easier. The interstitial is harder because standard Playwright almost always gets blocked — you need a stealth browser for that one.

If you’re dealing with the interstitial or want a deeper look at Cloudflare in general, check out the full Cloudflare bypass guide.


Requirements

  • Python 3.8+
  • Playwright installed
  • playwright-captcha — a library I built to handle this automatically
pip install playwright playwright-captcha
playwright install chromium

Two methods are covered here. Pick the one that fits your situation:

Click-based (free) — the browser clicks the Turnstile checkbox like a human. No external service. Fast and free, but works best with a stealth browser.

2Captcha API (paid) — sends the challenge to an external service that returns a token. More reliable, costs ~$3 per 1,000 solves, takes 10–30 seconds per solve.

Start with the click-based method. Switch to 2Captcha if you’re still getting blocked.


Method 1: Click-Based Solver

Here’s the thing about the click-based approach: it works by making the browser look human. Standard Playwright gets flagged immediately, so you need a stealth browser. The code below uses standard Playwright first, then I’ll show the stealth browser versions.

import asyncio
import logging
from playwright.async_api import async_playwright
from playwright_captcha import CaptchaType, ClickSolver, FrameworkType

logging.basicConfig(level='INFO')

async def solve_turnstile():
    async with async_playwright() as playwright:
        browser = await playwright.chromium.launch(headless=False)
        page = await browser.new_page()

        framework = FrameworkType.PLAYWRIGHT

        # Create the solver BEFORE navigating — it needs to attach early
        async with ClickSolver(framework=framework, page=page) as solver:
            await page.goto('https://2captcha.com/demo/cloudflare-turnstile')
            await asyncio.sleep(5)

            turnstile_container = page.locator('#cf-turnstile')
            await turnstile_container.wait_for()

            await solver.solve_captcha(
                captcha_container=page,
                captcha_type=CaptchaType.CLOUDFLARE_TURNSTILE
            )

        submit_button = page.locator('//button[@data-action="demo_action"]')
        await submit_button.wait_for()
        await submit_button.click()

        await asyncio.sleep(5)

        try:
            await page.locator('//p[text()="Captcha is passed successfully!"]').wait_for(timeout=10000)
            logging.info('Solved!')
        except Exception as e:
            logging.error(f'Failed: {e}')

asyncio.run(solve_turnstile())

One thing to note: create the solver before page.goto(). The library needs to attach listeners before the page loads, otherwise it misses the Turnstile widget initialization.

Standard Playwright gets detected by Cloudflare’s fingerprinting. Patchright patches Chrome at a deeper level and passes those checks. Swap out the import and you’re done.

pip install patchright
patchright install chromium
import asyncio
import logging
from patchright.async_api import async_playwright
from playwright_captcha import CaptchaType, ClickSolver, FrameworkType

logging.basicConfig(level='INFO')

async def solve_turnstile_patchright():
    async with async_playwright() as playwright:
        browser = await playwright.chromium.launch(channel="chrome", headless=False)
        context = await browser.new_context()
        page = await context.new_page()

        framework = FrameworkType.PATCHRIGHT

        async with ClickSolver(framework=framework, page=page) as solver:
            await page.goto('https://2captcha.com/demo/cloudflare-turnstile')
            await asyncio.sleep(5)

            turnstile_container = page.locator('#cf-turnstile')
            await turnstile_container.wait_for()

            await solver.solve_captcha(
                captcha_container=page,
                captcha_type=CaptchaType.CLOUDFLARE_TURNSTILE
            )

        submit_button = page.locator('//button[@data-action="demo_action"]')
        await submit_button.wait_for()
        await submit_button.click()

        await asyncio.sleep(5)

        try:
            await page.locator('//p[text()="Captcha is passed successfully!"]').wait_for(timeout=10000)
            logging.info('Solved!')
        except Exception as e:
            logging.error(f'Failed: {e}')

asyncio.run(solve_turnstile_patchright())

With Camoufox

Camoufox is Firefox-based and has more complete fingerprinting spoofing. It needs a small extra setup for playwright-captcha to work — the main_world_eval=True flag and the addon path.

pip install camoufox
camoufox fetch
import asyncio
import logging
import os
from camoufox import AsyncCamoufox
from playwright_captcha.utils.camoufox_add_init_script.add_init_script import get_addon_path
from playwright_captcha import CaptchaType, ClickSolver, FrameworkType

logging.basicConfig(level='INFO')

ADDON_PATH = get_addon_path()

async def solve_turnstile_camoufox():
    async with AsyncCamoufox(
        headless=False,
        geoip=True,
        humanize=True,
        main_world_eval=True,
        addons=[os.path.abspath(ADDON_PATH)]
    ) as browser:
        context = await browser.new_context()
        page = await context.new_page()

        framework = FrameworkType.CAMOUFOX

        async with ClickSolver(framework=framework, page=page) as solver:
            await page.goto('https://2captcha.com/demo/cloudflare-turnstile')
            await asyncio.sleep(5)

            turnstile_container = page.locator('#cf-turnstile')
            await turnstile_container.wait_for()

            await solver.solve_captcha(
                captcha_container=page,
                captcha_type=CaptchaType.CLOUDFLARE_TURNSTILE
            )

        submit_button = page.locator('//button[@data-action="demo_action"]')
        await submit_button.wait_for()
        await submit_button.click()

        await asyncio.sleep(5)

        try:
            await page.locator('//p[text()="Captcha is passed successfully!"]').wait_for(timeout=10000)
            logging.info('Solved!')
        except Exception as e:
            logging.error(f'Failed: {e}')

asyncio.run(solve_turnstile_camoufox())

Method 2: 2Captcha API

The API approach sends the Turnstile challenge to 2Captcha’s service, which solves it and returns a token. You don’t need a stealth browser for this — reliability comes from the token, not the browser fingerprint.

First, get an API key from 2captcha.com and add it to a .env file:

TWO_CAPTCHA_API_KEY=your_api_key_here

Install the extra dependencies:

pip install 2captcha-python python-dotenv
import asyncio
import logging
import os
from dotenv import load_dotenv
from playwright.async_api import async_playwright
from twocaptcha import AsyncTwoCaptcha
from playwright_captcha import CaptchaType, TwoCaptchaSolver, FrameworkType

logging.basicConfig(level='INFO')
load_dotenv()

TWO_CAPTCHA_API_KEY = os.getenv('TWO_CAPTCHA_API_KEY')

async def solve_turnstile_api():
    async with async_playwright() as playwright:
        browser = await playwright.chromium.launch(headless=False)
        context = await browser.new_context()
        page = await context.new_page()

        framework = FrameworkType.PLAYWRIGHT

        async with TwoCaptchaSolver(
            framework=framework,
            page=page,
            async_two_captcha_client=AsyncTwoCaptcha(TWO_CAPTCHA_API_KEY)
        ) as solver:
            await page.goto('https://2captcha.com/demo/cloudflare-turnstile')
            await asyncio.sleep(5)

            turnstile_container = page.locator('#cf-turnstile')
            await turnstile_container.wait_for()

            await solver.solve_captcha(
                captcha_container=turnstile_container,
                captcha_type=CaptchaType.CLOUDFLARE_TURNSTILE,
            )

        submit_button = page.locator('//button[@data-action="demo_action"]')
        await submit_button.wait_for()
        await submit_button.click()

        await asyncio.sleep(5)

        try:
            await page.locator('//p[text()="Captcha is passed successfully!"]').wait_for(timeout=10000)
            logging.info('Solved!')
        except Exception as e:
            logging.error(f'Failed: {e}')

asyncio.run(solve_turnstile_api())

Note the difference: with the API solver, you pass turnstile_container (the specific widget element) rather than the full page. The library uses this to extract the sitekey automatically.


Which method should you use?

Click Solver2Captcha API
CostFree~$3 per 1,000 solves
Speed2–5 seconds10–30 seconds
Needs stealth browserYes (strongly recommended)No
ReliabilityHigh with Patchright/CamoufoxVery high
Works headlessYesYes

If you want free and fast, use the click solver with Patchright. If you want maximum reliability without worrying about stealth browsers, use 2Captcha.


Adjusting retry behavior

Both solvers accept retry configuration:

# Click solver — more retries for tricky sites
solver = ClickSolver(
    framework=framework,
    page=page,
    max_attempts=5,
    attempt_delay=3
)

# API solver — longer delays for slow services
solver = TwoCaptchaSolver(
    framework=framework,
    page=page,
    async_two_captcha_client=AsyncTwoCaptcha(TWO_CAPTCHA_API_KEY),
    max_attempts=3,
    attempt_delay=10,
    sitekey='your_sitekey_here'  # pass manually if auto-detection fails
)

Common problems

Turnstile keeps failing with standard Playwright Cloudflare detects the browser fingerprint before any interaction. Switch to Patchright or Camoufox — both are in the examples above.

add_init_script error with Camoufox You’re missing main_world_eval=True or the addon path in the Camoufox constructor. Both are required for playwright-captcha to work with Camoufox.

Sitekey not detected automatically Pass it manually with sitekey='your_sitekey'. You can find the sitekey in the page HTML — look for data-sitekey in the Turnstile widget element.

Still getting blocked even with Patchright Make sure you’re using a clean proxy. If your IP is already flagged by Cloudflare, no amount of browser patching will help. Residential proxies work best here.


The library behind this

All of the examples here use playwright-captcha — an open-source library that handles Turnstile, reCAPTCHA v2/v3, and more. It supports multiple solving services and both click-based and API-based approaches.

Give it a star if it helped.