Tutorial
Selenium and Cloudflare Turnstile: undetected-chromedriver vs API
What undetected-chromedriver and SeleniumBase UC Mode do about Cloudflare Turnstile, where they stop, and a Selenium fallback that gets a token from an API.
By ZeroCaptcha Engineering4 min readPublished Updated
undetected-chromedriver is a patched ChromeDriver that removes the traces that give away an automated Chrome, and SeleniumBase’s UC Mode builds on it, adding methods that click the Cloudflare Turnstile checkbox with real mouse input. Both raise the chance that the widget passes in your browser. Neither changes your IP address, both need a visible browser to work well, and Turnstile can still refuse. The dependable setup gives the widget a few seconds and then falls back to a token from a solving API. This article compares the tools and shows that fallback in Selenium.
Automate only sites you are allowed to. See responsible captcha automation.
undetected-chromedriver
- What it is: a drop-in replacement for Selenium’s Chrome driver,
import undetected_chromedriver as ucthenuc.Chrome(), that patches the driver so anti-bot scripts do not see the usual webdriver traces. - What its README warns: it “DOES NOT hide your IP address”, so running from a data center makes detection much more likely, and headless mode is not officially supported.
- Release: version 3.5.5, uploaded to PyPI on 17 February 2024 (checked 1 October 2026). Chrome has changed a great deal since, which is worth knowing before you depend on it.
SeleniumBase UC Mode
- What it is: SeleniumBase’s mode “based on undetected-chromedriver”, with fixes and extra methods, in an actively released package (4.54.13 on PyPI, checked 1 October 2026).
uc_open_with_reconnect(url, seconds)opens a page while the driver is disconnected, so the page’s scripts cannot see it during load.uc_gui_click_captcha()finds a Turnstile or reCAPTCHA checkbox and clicks it with PyAutoGUI, real operating-system mouse input rather than a WebDriver command.- Its caveats: its docs say “UC Mode is detectable in Headless Mode”, and PyAutoGUI needs a
display; on a Linux server that means a virtual one (
xvfb=True).
Where browser-side tools stop
Both tools work on the browser’s side of the check. Cloudflare decides on its side, from signals that, by its own privacy addendum, include the client IP address, the TLS fingerprint and the User-Agent header. Its testing docs add that “Automated testing suites (like Selenium, Cypress, or Playwright) are detected as bots by Turnstile”. Two consequences:
- A clean browser on a flagged network can still fail. The IP warning in the undetected-chromedriver README is the same limit, stated plainly.
- Clicking is not a token. When the widget stays unsolved, or shows an error, your code has no token to submit, however well the browser is disguised.
A solving API sits outside that contest: it returns a token for the page’s sitekey, and your code submits it. The two approaches combine well.
The fallback in Selenium
It needs Python 3.10 or later, as current Selenium and requests releases do. Install selenium
and requests, set ZEROCAPTCHA_API and ZEROCAPTCHA_KEY, and save this as login.py. It uses plain Selenium; to use undetected-chromedriver, replace
webdriver.Chrome() with uc.Chrome() and nothing else changes.
import osimport timeimport uuid
import requestsfrom selenium import webdriverfrom selenium.common.exceptions import TimeoutExceptionfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support.ui import WebDriverWait
API = os.environ["ZEROCAPTCHA_API"]KEY = os.environ["ZEROCAPTCHA_KEY"]
def solve_turnstile(page_url, sitekey, action=None, cdata=None): task = {"type": "TurnstileTaskProxyless", "websiteURL": page_url, "websiteKey": sitekey, "metadata": {}} # The widget's data-action and data-cdata go in metadata, only when it sets them: many sites # check both when they verify the token. if action: task["metadata"]["action"] = action if cdata: task["metadata"]["cdata"] = cdata # One Idempotency-Key per task: a retried create with it returns the same task. created = requests.post(f"{API}/createTask", json={"clientKey": KEY, "task": task}, headers={"Idempotency-Key": str(uuid.uuid4())}, timeout=15).json() if created["errorId"]: raise RuntimeError(f"createTask: {created['errorCode']}") deadline = time.monotonic() + 180 while time.monotonic() < deadline: time.sleep(2) result = requests.post( f"{API}/getTaskResult", json={"clientKey": KEY, "taskId": created["taskId"]}, timeout=15 ).json() if result["errorId"]: raise RuntimeError(f"getTaskResult: {result['errorCode']}") if result["status"] == "ready": return result["solution"]["token"] raise TimeoutError("no token within 180 seconds")
def widget_token(driver): """The token the widget itself wrote, or an empty string.""" return driver.execute_script( "const input = document.querySelector('[name=\"cf-turnstile-response\"]');" "return input ? input.value : '';" )
driver = webdriver.Chrome()try: driver.get("https://shop.example.com/login") widget = WebDriverWait(driver, 15).until(lambda d: d.find_element(By.CSS_SELECTOR, "[data-sitekey]"))
try: # 1. Give the widget ten seconds to pass on its own. WebDriverWait(driver, 10).until(widget_token) except TimeoutException: # 2. Otherwise, get a token from the API and put it where the widget would have. token = solve_turnstile( driver.current_url, widget.get_attribute("data-sitekey"), widget.get_attribute("data-action"), widget.get_attribute("data-cdata"), ) driver.execute_script( "for (const input of document.querySelectorAll('[name=\"cf-turnstile-response\"]'))" " input.value = arguments[0];", token, )
driver.find_element(By.ID, "email").send_keys("me@example.com") driver.find_element(By.ID, "password").send_keys(os.environ.get("SHOP_PASSWORD", "")) driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click() WebDriverWait(driver, 15).until(lambda d: "/login" not in d.current_url) print("Logged in:", driver.current_url)finally: driver.quit()WebDriverWait.until calls widget_token until it returns something non-empty, so the first
wait ends as soon as the widget writes its token. Selenium 4.6 and later download a matching
ChromeDriver on their own (Selenium Manager), so webdriver.Chrome() needs no setup.
Choosing
| undetected-chromedriver | SeleniumBase UC Mode | Solving API fallback | |
|---|---|---|---|
| Works headless | Not officially | Detectable, per its docs | Yes: no browser needed for the token |
| Needs a display | No | For GUI clicks, yes | No |
| Depends on your IP’s reputation | Yes | Yes | The token does not; your form submission still comes from your IP |
| Cost | Free | Free | Charged per solved token, only when the widget did not pass |
| Maintenance | Last release February 2024 | Active | Maintained by the API provider |
Using both keeps costs down when the widget passes on its own, and keeps the run going when it does not. A Cloudflare Turnstile token lasts 300 seconds and works once, so submit straight after the fallback: see Cloudflare Turnstile token expiry.
Sources
- undetected-chromedriver README and PyPI (checked 1 October 2026).
- SeleniumBase UC Mode and PyPI (checked 1 October 2026).
- Cloudflare Turnstile: testing and privacy addendum (checked 1 October 2026).
- Selenium Manager (checked 1 October 2026).
The team that builds and runs the ZeroCaptcha API. Articles are drafted with AI tools, then checked against the API's code and the primary sources each one cites.