undetected-chromedriver and Cloudflare Turnstile
undetected-chromedriver does one narrow thing well: it patches the window.cdc_… marker out of the chromedriver binary so a page cannot read the most obvious sign of automation. It contains no Turnstile-specific code, its README states plainly that it does not hide your IP address, and the package has had one maintenance merge in eighteen months. Knowing that up front saves the week people spend tuning options.
What the patch actually is
It is worth seeing, because it is smaller than its reputation. patcher.py opens the chromedriver executable, finds the injected window.cdc_… code block by regular expression, and overwrites it with a harmless console.log padded to the same length:
match_injected_codeblock = re.search(rb"\{window\.cdc.*?;\}", content) if match_injected_codeblock: target_bytes = match_injected_codeblock[0] new_target_bytes = ( b'{console.log("undetected chromedriver 1337!")}'.ljust( len(target_bytes), b" " ) ) new_content = content.replace(target_bytes, new_target_bytes)
That is the mechanism. Chromedriver normally injects variables named cdc_… into every document, which any page can read in one line; the patch removes that tell. Chrome is a subclass of Selenium's own WebDriver, so everything else is ordinary Selenium.
There is no Turnstile-specific code anywhere in the package — no widget detection, no clicking, no token handling. Nothing about it targets Cloudflare in particular.
Two things the README tells you that most guides skip
The project is unusually direct about its own limits, and both statements matter more than anything in a tutorial.
"THIS PACKAGE DOES NOT, and i repeat DOES NOT hide your IP address, so when running from a datacenter (even smaller ones), chances are large you will not pass! Also, if your ip reputation at home is low, you won't pass!"
That is the maintainer, in the README, naming the exact failure people report as a mystery. If a script clears the challenge on a laptop and loops forever in Docker, the package's own documentation predicted it — Turnstile fails on a datacenter IP goes through what Cloudflare documents about that.
The second, on headless mode: "just to mention it another time, since some people have hard time reading: headless is still WIP. Raising issues is needless". So a headless server deployment is running an explicitly unfinished path.
The issue tracker is closed, and the links you will find are dead
Search for this problem and you will land on issue numbers. They no longer resolve. Issues are disabled on the repository — the API returns 410 Issues are disabled for this repo and the web URLs return 404. The README explains why:
"I will be putting limits on the issue tracker. It has beeen abused too long. any good news? Yes, i've opened Undetected-Discussions which i think will help us better in the long run."
Three of the four issues most often cited are recoverable from archive snapshots, and it is worth correcting a claim that circulates about them. They are not all titled "Turnstile challenge failed":
| Issue | Actual title | Opened |
|---|---|---|
| 827 | *Cloudflare turnstile* | October 2022 |
| 1462 | *Detected: CloudFlare Turnstile Challenge Failed* | August 2023 |
| 1175 | *Not bypassing cloudflare turnstile on VPS* | April 2023 |
| 2171 | No archive snapshot exists — unverifiable | — |
Note what #1175 is actually about: a configuration that "works great on my local computer" and does not on a VPS. It is the datacenter-IP problem, filed as a library bug. The discussion has since moved to GitHub Discussions, where several Turnstile threads are open.
Maintenance status, stated plainly
| Signal | Value |
|---|---|
| Latest PyPI release | 3.5.5, uploaded February 2024 |
| GitHub releases | None published |
| Last commit to master | July 2025 — a single merge |
| Issue tracker | Disabled |
| Stars | 12,806 |
This is not a criticism of the maintainer, who has said what they are doing: nodriver is described in its own README as "the official successor of the Undetected-Chromedriver python package". It is a fact to weigh before building on a package whose last release predates the current Turnstile widget by a long way. See nodriver and Turnstile for what the successor does and does not do.
Using it, and where the token comes from
The basic form is one import and one line — that simplicity is genuinely the appeal:
import undetected_chromedriver as uc driver = uc.Chrome() driver.get("https://nowsecure.nl")
Because Chrome subclasses Selenium's WebDriver, everything else is standard Selenium — find_element(By.CSS_SELECTOR, …), send_keys, execute_script. That also means the token handling is the same as any Selenium script: write the token into the hidden field the widget would have filled, then submit.
import os import json import urllib.request import undetected_chromedriver as uc 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"] driver = uc.Chrome() driver.get(PAGE) # The widget writes into a hidden input; write into the same one, then fire the # events a framework would be listening for. driver.execute_script( """ const [name, value] = arguments; let el = document.querySelector(`[name="${name}"]`); if (!el) { el = document.createElement("input"); el.type = "hidden"; el.name = name; document.querySelector("form").appendChild(el); } el.value = value; el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); """, "cf-turnstile-response", token, )
The field name is cf-turnstile-response unless the page renamed it with data-response-field-name — our checker reports the real one along with the sitekey. Tokens are single-use and expire in minutes, so fetch one at the moment you submit; token lifetime has the rules.
A token typically comes back in under 1.5 seconds. Prepaid credits start at $0.40 per 1,000 and fall to $0.075 at volume, failed solves are never billed, and the first 1,000 are free. Solve only against properties you own or are authorised to test.
Common questions
It has no Turnstile-specific code at all. What it does is binary-patch the window.cdc_ marker out of the chromedriver executable, which removes one obvious automation tell. Whether a challenge then passes depends on everything else — IP reputation above all, which the README says explicitly the package does not address.
The README answers this directly: it does not hide your IP address, and running from a datacenter means you will probably not pass. A residential address with good reputation buys margin that a server address does not have, so the same code behaves differently for reasons that have nothing to do with the code.
No. Issues are disabled on the repository, so the URLs return 404 and the API returns 410. Three of the four commonly cited ones are recoverable from web archives, and only one of them is actually titled "Turnstile Challenge Failed" — the others are "Cloudflare turnstile" and "Not bypassing cloudflare turnstile on VPS". The conversation moved to GitHub Discussions.
Barely. The last PyPI release is 3.5.5 from February 2024, there are no GitHub releases, and master has had a single merge since. The maintainer describes nodriver as the official successor, so treat this as a package in maintenance rather than one tracking a moving target.
You can, and the README says headless is still work in progress and asks people not to file issues about it. On a server the usual approach is a virtual display such as Xvfb rather than true headless — that keeps you on the supported path and avoids one whole class of detection.
Related
Sources
More in Guides
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