guides · patchright

Patchright and Cloudflare Turnstile

Patchright is Playwright with four specific leaks patched out, and one of them matters more than the rest for Turnstile: it can interact with elements inside closed shadow roots. That is precisely where the Turnstile checkbox lives, and it is why ordinary Playwright selectors cannot reach it. It is also the base that Theyka/Turnstile-Solver, the best-known open-source Turnstile tool, is built on.

What it patches, and why each one matters

The README lists four patches. They are worth going through individually because they are not equally important and the trade-offs are real.

The Runtime.enable leak — the project calls this "the biggest Patch Patchright uses". Enabling the CDP Runtime domain is observable from inside the page, and it is what a lot of automation detection keys on. Patchright avoids it by executing JavaScript in isolated execution contexts instead.

The Console.enable leak — patched by disabling the Console API altogether. Read that carefully, because it has a cost the README states plainly: "This means, console functionality will not work in Patchright." If your test suite asserts on console output, or you debug by reading it, that is gone.

Command flags--disable-blink-features=AutomationControlled added; --enable-automation, --disable-popup-blocking, --disable-component-update, --disable-default-apps and --disable-extensions removed. The removals are as deliberate as the addition: a browser with extensions disabled and component updates off is itself an unusual browser.

Closed shadow roots — "Patchright is able to interact with elements in Closed Shadow Roots. Just use normal locators and Patchright will do the rest." This is the one that decides whether you can touch a Turnstile widget at all.

Why closed shadow roots are the Turnstile problem

A Turnstile widget is not markup you can select. api.js inserts an iframe served from challenges.cloudflare.com, and the surrounding structure uses a shadow root in closed mode — meaning element.shadowRoot returns null even from your own page's JavaScript. That is the whole point of closed mode.

So the ordinary approaches fail for an ordinary reason. A CSS selector does not descend into a shadow root. page.frame_locator() gets you into the iframe but not past the closed boundary around it. Scripts that try querySelector on the checkbox find nothing, and the usual conclusion — that the widget is defended against clicking — is a level too abstract. It is not reachable in the first place.

Patchright's patch removes that specific obstacle, which is why the drop-in import is often the entire fix people were looking for:

python
# The only change from Playwright is this import.
from patchright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("http://playwright.dev")
    page.screenshot(path=f"example-{p.chromium.name}.png")
    browser.close()

Chromium only. The README is explicit: "Patchright only patches CHROMIUM based browsers. Firefox and Webkit are not supported." If you need Firefox, Camoufox is the fork that takes that position.

The recommended configuration says headless is not it

Patchright's own best-practice snippet launches a persistent context on real Chrome, with headless=False and no viewport override:

python
playwright.chromium.launch_persistent_context(
    user_data_dir="...",
    channel="chrome",
    headless=False,
    no_viewport=True,
    # do NOT add custom browser headers or user_agent
)

Three things are being said there. Real Chrome rather than bundled Chromium. A persistent profile rather than a fresh one every run. And no custom headers or user agent — because a hand-set user agent that disagrees with the browser actually running is a stronger signal than the default would have been.

The project's stealth claim is a self-assessment and should be read as one: "With the right setup, Patchright currently is considered undetectable", listing Brotector, Cloudflare, Kasada, Akamai and Shape/F5 as passed. It is the maintainers' own testing, not an independent result, and "currently" is doing real work in that sentence — this is a field where a claim has a shelf life.

Theyka/Turnstile-Solver, and what it actually does

It is the best-known open-source Turnstile tool and it is built on Patchright — its requirements.txt is five lines, of which two are patchright and camoufox[geoip], and api_solver.py imports both.

The interesting part is the technique, because it is not what people assume. It does not solve the challenge on the target site. It builds its own local page containing the widget, using the target's sitekey, and harvests the token from that:

python
turnstile_div = (
    f'<div class="cf-turnstile" data-sitekey="{sitekey}"'
    + (f' data-action="{action}"' if action else "")
    + (f' data-cdata="{cdata}"' if cdata else "")
    + "></div>"
)
page_data = self.HTML_TEMPLATE.replace("<!-- cf turnstile -->", turnstile_div)

# …then read the token out of the field the widget fills
turnstile_check = page.input_value("[name=cf-turnstile-response]")

That is the same architecture any hosted solver uses, which is worth understanding whichever route you take: a token is produced by rendering the widget somewhere, and it is the sitekey and page URL that bind it — not where the browser was.

Two things to check before depending on it. The licence badge says CC BY-NC 4.0, which is non-commercial, so it is not a drop-in for a commercial product without resolving that. And the repository's last push is over a year old, which in this ecosystem is a long time.

When to hand the token over instead

Patchright makes the widget reachable. Whether the interaction is accepted is a different question, and Cloudflare's position on the category is that "Browser automation frameworks, such as Selenium, Puppeteer, Playwright, and Cypress, are not supported for solving production challenges." Patchright is Playwright with patches; it sits inside that sentence.

If the target is your own and the widget is only in the way of a test, the dummy sitekeys remove it from the test entirely — Cloudflare's documented recommendation, and the whole reachability problem disappears with it.

If the challenge does have to clear, fetching a token is less fragile than clicking, because it does not depend on the widget rendering somewhere an interaction can land:

python
import json, os, urllib.request
from patchright.sync_api import sync_playwright

SITEKEY = "0x4AAAAAAA_target"
PAGE = "https://app.example.com/login"

req = urllib.request.Request(
    "https://api.solvegate.io/v1/solve",
    data=json.dumps({"gate": "turnstile", "sitekey": SITEKEY, "url": PAGE}).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['SOLVEGATE_KEY']}",
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req, timeout=30) as res:
    token = json.load(res)["token"]

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto(PAGE)
    page.eval_on_selector(
        '[name="cf-turnstile-response"]',
        "(el, value) => { el.value = value; "
        "el.dispatchEvent(new Event('input', { bubbles: true })); }",
        token,
    )
    page.click("button[type=submit]")
    browser.close()

A token typically returns in under 1.5 seconds. Prepaid credits run from $0.40 per 1,000 down to $0.075 at volume; failed solves are never billed; the first 1,000 are free. Solve only against properties you own or are authorised to test.

Common questions

Four patches, but the decisive one is closed shadow roots: Patchright can interact with elements inside them using normal locators. The Turnstile checkbox lives inside a closed shadow root, which is why ordinary Playwright selectors return nothing — the element is not defended so much as unreachable.

For Chromium, yes — the README calls it that and the only change in the basic example is the import. Firefox and WebKit are explicitly not supported. One behavioural difference to know about: the Console.enable patch works by disabling the Console API entirely, so console functionality does not work.

Its own recommended configuration uses headless=False, on real Chrome via channel="chrome", with a persistent user data directory and no custom user agent or headers. That is the maintainers' best-practice snippet rather than a hard requirement, but it is a clear signal about where the tool is strongest.

Check the licence first — the badge says CC BY-NC 4.0, which is non-commercial. The repository has also not been pushed to in over a year, which is a long time in this ecosystem. Its technique is worth understanding regardless: it renders the widget on its own local page using the target's sitekey and reads the token out.

No. Cloudflare states that browser automation frameworks including Playwright are not supported for solving production challenges, and Patchright is Playwright with patches. What it changes is reachability and a set of specific leaks — a different thing from being an accepted client.

Related

Sources

More in Guides

Playwright and Cloudflare TurnstileStop Cloudflare Turnstile breaking Playwright tests: use Cloudflare's official test sitekeys on staging, or solve a real widget and inject the token.Python Cloudflare Turnstile SolverSolve Cloudflare Turnstile from Python with plain requests or the solvegate SDK. Complete runnable code, async usage, retries, and error handling.Node.js Turnstile solverSolve Cloudflare Turnstile from Node.js: a zero-dependency global fetch version, the solvegate npm SDK, async/await error handling and TypeScript types.How to find a Turnstile sitekey on a pageFind a Cloudflare Turnstile sitekey in seconds: the data-sitekey attribute, keys starting 0x4, explicit-render calls, and Playwright/Node code that extracts it.How to get the cf-turnstile-response tokenWhat the cf-turnstile-response token is, the hidden input it lives in, how siteverify validates it, and how to get one in your own automated tests.Puppeteer Turnstile bypassHow to handle Cloudflare Turnstile in Puppeteer: Cloudflare's official test sitekeys for pages you own, plus a runnable token-injection script.Selenium and Cloudflare Turnstile in PythonHandle Cloudflare Turnstile in Selenium 4 and Python: test sitekeys, explicit waits, execute_script token injection, and what changes in headless.Turnstile fails on a datacenter IPThe same code passes on your laptop and loops forever on a server. What Cloudflare documents about IP reputation, what it does not, and what actually changes.curl_cffi and Turnstilecurl_cffi impersonates TLS and HTTP/2 fingerprints and has no JavaScript runtime. Its own FAQ says so. What that solves, what it cannot, and where the line is.Scrapy and TurnstileScrapy has no JavaScript engine, so a Turnstile page returns markup and no data. The three documented routes, and which one Scrapy's own docs recommend first.undetected-chromedriver and TurnstileIt binary-patches one string out of chromedriver. Its own README says it does not hide your IP and that headless is unfinished. What that means for Turnstile.SeleniumBase and TurnstileThe uc_gui captcha methods use PyAutoGUI and raise in headless mode — the check is in the source. solve_captcha uses CDP and does not. Which to use where.Camoufox and TurnstileCamoufox is a Firefox fork that patches fingerprints at the C++ level. It ships no Turnstile solver, and the issue asking for one was closed as not planned.nodriver and Turnstilenodriver's README documents a tab.cf_verify() that clicks the Cloudflare checkbox. It is not in the shipped code. What the library does do instead.DrissionPage and TurnstileDrissionPage reaches into non-open shadow roots and switches between browser and HTTP mode. It also forbids commercial use, and its docs are Chinese-only.Go, Colly and TurnstileColly has no JavaScript engine — its dependency graph proves it. What that means for Turnstile in Go, and how chromedp and rod compare when you need a browser.

Automating a gate you own or are authorised to test?

// SolveGate clears Cloudflare Turnstile and WAF challenges through one REST call · first 1,000 solves free