If your undetected-chromedriver script worked last year and now hangs on 'Just a moment...', throws 403s, or watches a Turnstile widget spin forever, you are not doing anything wrong. The library has not fundamentally broken. Cloudflare has simply moved its detection past the tells that undetected-chromedriver was built to hide.
This post explains, in plain terms, what undetected-chromedriver (UC) actually does, why modern Cloudflare catches it anyway, and how to read the symptoms you are seeing. We will start with the honest no-cost fixes and exactly where they hit a wall, then cover the pragmatic path when you need automation that does not break every Cloudflare update: keep driving the browser but offload the challenge, or drop the browser entirely and fetch a cf_clearance over a simple HTTP call.
What undetected-chromedriver actually does
undetected-chromedriver is a patched build of Selenium's ChromeDriver. Vanilla ChromeDriver leaves obvious fingerprints behind: it sets navigator.webdriver to true, injects a cdc_ variable into the document object that bot detectors grep for, and trips a handful of Chrome DevTools Protocol (CDP) tells. UC patches the ChromeDriver binary and the launch flags to strip those signals, so a freshly started browser looks much more like a human's Chrome than a default Selenium session does.
For a long time that was enough. Cloudflare's older checks leaned heavily on exactly those automation tells, so removing them was often the difference between a 403 and a 200. UC became the default recommendation for 'scrape a Cloudflare site with a real browser' precisely because it papered over the easy giveaways. The problem in 2026 is that those easy giveaways are no longer what Cloudflare is looking at.
Why modern Cloudflare catches it anyway
Cloudflare's current bot management does not rely on a single tell. It layers several independent detection systems, and undetected-chromedriver only addresses the oldest layer of them:
- Turnstile and managed challenges UC cannot pass: Turnstile runs a non-interactive proof-of-work plus browser-API probing and only then issues a cf-turnstile-response token. A managed challenge dynamically picks its checks per request. UC can render these widgets, but it has no logic to actually produce a valid token or clear a hard managed challenge, so the page never advances.
- CDP and runtime leaks: UC drives Chrome over the DevTools Protocol, and that connection itself is observable. Detectors probe for CDP artifacts, timing anomalies, and runtime objects that only exist when a browser is being automated. Patching navigator.webdriver does not remove the CDP surface underneath.
- TLS / JA3 fingerprint: before any JavaScript runs, Cloudflare inspects the TLS ClientHello. The handshake from an automation stack can drift from a genuine consumer Chrome, and a JA3/JA4 mismatch is a flag on the very first packet, no matter how clean the JS environment looks.
- navigator.webdriver and stack-trace tells: even with the obvious flags patched, error stack traces, injected function signatures, and subtle property descriptor differences betray the automation. Modern detection scripts read these higher-order tells, not just the boolean UC fixes.
- Slowing maintenance: the cat-and-mouse only works if the patch keeps up with Chrome and Cloudflare releases. UC's maintenance has slowed, and the same maintainer now points people at nodriver, a successor that talks to Chrome with far fewer CDP traces. If you are still on UC, part of your problem may simply be that the patch is behind.
Decoding the symptoms you are seeing
The error you get tells you which layer caught you. Matching the symptom to the cause saves you from guessing:
- Stuck on 'Just a moment...': the interstitial loaded but the challenge never cleared. Your browser is being asked to prove itself and is failing the check, either because of CDP/runtime leaks or because it is a hard managed challenge UC cannot solve.
- Plain 403 (or error 1020): you were flagged before or during the handshake, typically a TLS/JA3 mismatch or a low-reputation datacenter IP. Cloudflare refused you outright rather than even serving a challenge.
- Turnstile widget never clears: the form rendered a Turnstile box that spins or resets. UC has no token-solving path, so the widget never produces a usable cf-turnstile-response and your submit fails.
- Detected even with headless=False: a common surprise. Running non-headless removes the headless tells, but it does nothing about CDP traces, the TLS fingerprint, or a managed challenge. A visible window is not the same as an undetectable one.
Free fixes worth trying first
Before paying for anything, exhaust the no-cost options. For lighter Cloudflare configurations these genuinely work, and there is no reason to over-engineer a site that only does passive checks.
Work through them in order of effort:
- Upgrade UC and Chrome: a surprising share of 'undetected-chromedriver not working' reports are just a stale patch against a newer Chrome. Pin matching versions and update before anything else.
- Switch to nodriver: from the same maintainer, nodriver drops the Selenium/ChromeDriver layer entirely and drives Chrome directly with far fewer CDP traces. It is the intended modern replacement and often clears sites UC no longer can.
- Use a persistent user-data-dir: reuse a real Chrome profile so cookies, local storage, and a prior cf_clearance survive between runs. A warm profile looks far more human than a fresh one challenged on every launch.
- Run non-headless on a real display: headless mode adds tells. A genuine windowed browser (or xvfb on a server) removes that particular signal, though as noted above it is not a cure on its own.
- Route through clean residential proxies: datacenter IPs are flagged fast. Rotating residential or mobile IPs raise your reputation score and reduce blanket 403s.
Where the free fixes stop
Be honest with yourself about the ceiling here. Even a fully tuned UC or nodriver setup carries hard limits that no amount of flag-patching removes:
Driving a real browser is heavy: each instance burns hundreds of megabytes of RAM and real CPU, so scaling to many concurrent workers gets expensive and fragile fast. The stack is still fingerprintable, because CDP traces, TLS drift, and higher-order runtime tells remain even after the obvious flags are gone. And critically, none of these tools solve a Turnstile token: they can show you the widget, but they cannot mint a valid cf-turnstile-response, so any Turnstile-gated form stays closed. When the target serves a hard managed challenge or a Turnstile widget, browser patching alone simply runs out of road.
The pragmatic path: offload the hard part
Once you hit the layers a patched browser cannot beat, the realistic move is to stop trying to out-patch Cloudflare and instead hand the specific hard step to a service built for it. There are two shapes to this.
First, you can keep driving the browser for everything that works and only call out to a managed API for the Turnstile token or the managed-challenge clearance. Second, and usually simpler, you can drop the browser entirely: many scraping jobs do not need a real browser once you hold a valid cf_clearance cookie, so you fetch that cookie over a single HTTP call and run the rest of your pipeline on a plain requests session at native speed.
NSLSolver is a developer API built for exactly this offload. It handles Cloudflare Turnstile, Cloudflare Challenge (managed/JS challenge), and Kasada behind one HTTP contract. It does not solve reCAPTCHA, hCaptcha, DataDome, or image captchas, so this is the right tool specifically when Cloudflare or Kasada is your wall. Turnstile solves average around 250ms with a 99.9% success rate, billing is per successful solve so failed solves are free, and pricing is pay-as-you-go: Turnstile at $0.40 per 1,000 solves, Cloudflare Challenge at $0.50 per 1,000, and Kasada at $1.50 per 1,000. New accounts get 100 free requests at signup, with no card and no crypto needed to start.
Fetching a cf_clearance with NSLSolver in Python
Here is the version that replaces the browser entirely for a managed/JS challenge. You POST the target URL to /solve and get back the cookies and the User-Agent you must replay on your own session. The cf_clearance value lives inside a cookies dictionary keyed by cf_clearance, so apply it with session.cookies.update(...) and set the User-Agent header to the returned user_agent so your fingerprint matches the one the cookie was issued for.
Note the field names exactly: the response field is user_agent (snake_case), and cookies is a dict, not a list. Cloudflare binds the clearance cookie to that specific User-Agent, so always send back the value the API returns rather than your own. If you are keeping the browser, request a Turnstile token instead by posting type 'turnstile' with the widget site_key and page url, then inject the returned token as the cf-turnstile-response value.
import requests
API = "https://api.nslsolver.com"
HEADERS = {"X-API-Key": "nsl_YOUR_API_KEY"}
TARGET = "https://target-site.com"
# 1) Hand the Cloudflare managed/JS challenge to NSLSolver
resp = requests.post(
f"{API}/solve",
headers=HEADERS,
json={
"type": "challenge",
"url": TARGET,
"proxy": "http://user:pass@host:port",
},
timeout=120,
)
data = resp.json()
# 2) Replay the cf_clearance cookie + matching User-Agent
# on a plain requests session -- no browser needed
session = requests.Session()
session.cookies.update(data["cookies"]) # dict keyed by cf_clearance
session.headers["User-Agent"] = data["user_agent"] # snake_case field
# 3) Scrape at native speed -- the request looks like a cleared browser
page = session.get(TARGET)
print(page.status_code) # 200
print(page.text[:500])