The 2026 Nodriver guide for production scraping
I’ve run selenium, playwright, and puppeteer against the same set of protected targets for years, and the pattern repeats. The moment a site’s bot detection team notices your traffic pattern, they patch it, and your driver gets flagged within weeks. Nodriver is the project that grew out of that cycle. It’s built by the same developer who wrote undetected-chromedriver, and it drops the WebDriver protocol entirely, talking to Chrome directly over the Chrome DevTools Protocol instead. That one architectural choice removes a whole category of fingerprint checks that trip up selenium and even a stock playwright install.
This is for operators who already run some kind of scraping pipeline, proxies, a queue, maybe a headless browser, and are hitting walls on sites that specifically look for automation signatures: navigator.webdriver set to true, missing CDP artifacts, or headless-only rendering quirks. If you’re brand new to scraping, get a plain requests or Playwright script working first. Nodriver earns its complexity once you already know why you’re getting blocked.
By the end of this you’ll have a working Nodriver install, a stealth-configured browser routed through a residential proxy, session persistence across restarts, and a clear picture of what actually changes when you go from testing on a laptop to running a few thousand sessions a day.
what you need
- Python 3.10 or newer. Nodriver’s API is async-first and current asyncio patterns assume a modern interpreter, even though the package technically installs on older 3.8 builds.
- Google Chrome or Chromium installed on the host. Nodriver drives the real browser binary, it doesn’t bundle one the way some Playwright installs do.
- the package itself:
pip install nodriver, published on PyPI. - a proxy pool. Datacenter IPs are fine for low-protection targets but burn fast on anything running Cloudflare or PerimeterX. Residential or mobile proxies from a vendor like Decodo hold up longer, current pricing is in our Decodo review.
- a VPS or dedicated box once you go past a handful of concurrent sessions. Laptop RAM disappears quickly once you’re running more than five or six Chrome instances at once.
- basic comfort with Python’s asyncio. There’s no reliable sync wrapper for Nodriver worth using in production, it’s async or nothing.
- budget: figure roughly $2 to $15 per GB for residential proxy traffic depending on vendor and volume tier, plus VPS costs, usually $5 to $40 a month for a small box that handles low double digits of concurrent sessions.
step by step
1. install nodriver and confirm chrome is reachable
pip install nodriver
python -c "import nodriver; print(nodriver.__version__)"
expected output: a version string prints with no import error.
if it breaks: an ImportError usually means an incompatible Python version, upgrade to 3.10+. If Chrome isn’t found when you actually launch a browser (next step), pass browser_executable_path explicitly, Nodriver doesn’t guess across non-standard install locations reliably.
2. run a minimal script and confirm it launches
import nodriver as uc
async def main():
browser = await uc.start()
page = await browser.get("https://example.com")
await page.save_screenshot("test.png")
await browser.stop()
if __name__ == "__main__":
uc.loop().run_until_complete(main())
expected output: test.png is saved and shows example.com rendered normally.
if it breaks: if the browser opens but navigation hangs, check outbound network from the host. Some VPS providers restrict outbound traffic on the ports Chrome’s CDP connection needs, which isn’t obvious from a simple ping test.
3. verify the webdriver flag is actually gone
page = await browser.get("https://bot.sannysoft.com")
await page.save_screenshot("fingerprint.png")
expected output: the screenshot shows navigator.webdriver reading false or undefined, unlike a stock selenium run which flags red across most checks on that page. MDN documents navigator.webdriver as a property browsers expose specifically so sites can detect automated control. Nodriver avoids setting it because it never loads the WebDriver extension in the first place.
if it breaks: if navigator.webdriver still reads true, you’re likely launching a wrapped browser somewhere in your stack, some Docker base images inject automation flags at the OS level. Check your Dockerfile and any launch scripts upstream of the Python call.
4. route sessions through a proxy
browser = await uc.start(
browser_args=["--proxy-server=http://user:[email protected]:10000"]
)
expected output: outbound requests exit through the proxy IP, confirm against an IP-echo endpoint.
if it breaks: auth-in-URL proxy strings sometimes get dropped silently by Chrome’s proxy handling. If the exit IP doesn’t change, switch to an IP-allowlisted gateway at the vendor side, or run a small local proxy-auth forwarder in front of Chrome.
5. randomize the fingerprint surface
browser = await uc.start(
browser_args=[
"--window-size=1366,768",
"--lang=en-US",
]
)
expected output: each session presents a slightly different viewport and locale signature, checkable with the same sannysoft-style test page from step 3.
if it breaks: if every session still looks identical downstream, you’re probably reusing one browser profile directory across sessions. Give each session its own user_data_dir.
6. persist cookies and session state
browser = await uc.start(user_data_dir="./profiles/account_01")
expected output: logging in once and restarting the script later keeps you logged in, which means fewer login-triggered captchas on subsequent runs.
if it breaks: if state doesn’t persist, check that nothing in your cleanup logic is deleting the profile directory on exit. Crash-recovery code, including some default browser cleanup, will wipe a profile it thinks is corrupted.
7. add crash recovery and timeouts
import asyncio
async def safe_get(browser, url, retries=3):
for attempt in range(retries):
try:
return await asyncio.wait_for(browser.get(url), timeout=30)
except asyncio.TimeoutError:
if attempt == retries - 1:
raise
await asyncio.sleep(2 ** attempt)
expected output: transient hangs, a slow proxy hop or a stuck redirect, get retried instead of killing the whole run.
if it breaks: if retries never succeed, the problem usually isn’t the browser, it’s the proxy IP getting flagged mid-session. Rotate to a fresh IP on the second retry instead of hammering the same one.
8. validate against a real protected target
Run the full pipeline against a site you know sits behind Cloudflare or similar, and confirm you get the actual page rather than a challenge screen.
expected output: a normal 200 response with real content, no interstitial or CAPTCHA redirect.
if it breaks: if you’re still getting challenged, the issue is often timing rather than fingerprint. Human-paced navigation, scroll events, dwell time before clicking, matters as much as what Nodriver reports at the browser level. See our piece on bypassing Cloudflare 403s for the behavioral side, the same principles apply with Nodriver as the driver underneath.
common pitfalls
running headless without checking if the target cares. Nodriver launches headed by default, and that’s deliberate. Some detection systems specifically look for headless rendering artifacts in font shaping and GPU flags. Only go headless once you’ve confirmed the target doesn’t check for it, and use --headless=new on current Chrome builds, not the legacy headless mode.
reusing one profile directory across hundreds of accounts. This cross-contaminates cookies and fingerprints, and gets accounts linked and banned together instead of failing independently.
cheaping out on proxies for high-value targets. Datacenter IPs are fine for low-protection scraping but burn almost instantly on Cloudflare or PerimeterX-protected sites. Budget for residential or mobile proxies from the start on anything that matters.
forgetting to call browser.stop(). Orphaned Chrome processes pile up on a VPS and eat RAM until the box starts swapping and everything slows down. Always stop the browser in a finally block, not just on the happy path.
treating “undetected” as permanent. Fingerprint test pages and target-site detection both change over time. A config that passes today can fail after the next Cloudflare bot-management update. Re-test on a schedule instead of assuming a working setup stays working indefinitely.
scaling this
10x (a handful to about 10 concurrent sessions): a single VPS is enough. Run sessions sequentially or gate concurrency with an asyncio.Semaphore, one proxy vendor account, manual restarts when something crashes. Nothing here needs orchestration.
100x: you need a queue, redis or something similar, feeding a worker pool, with each worker owning its own profile directory and sticky proxy session. Move to Docker so one crashed Chrome process doesn’t take down the rest of the fleet. Watch concurrent-session caps on your proxy plan, most residential tiers limit how many sessions can be open at once regardless of bandwidth. Monitoring block rate per proxy pool stops being optional at this point, you’ll want to catch degradation before it shows up as a revenue problem.
1000x: this is a distributed systems problem more than a scraping one. You’ll want multiple VPS or regions so a single egress IP range doesn’t get rate-limited by the target’s CDN, proper retry and backoff logic in the queue, and cost per successful session as your core metric, proxy spend plus compute divided by pages actually returned, not requests sent. Run your own fingerprint tests against your own output before it ships, so you catch a regression before the target site’s detection does. At this volume you’ll typically negotiate custom proxy pricing instead of paying list rate, see how vendors structure volume tiers in our Decodo vs SOAX comparison. If you’re also running antidetect browser profiles for account-based work alongside pure page scraping, the writeups at antidetectreview.org/blog/ cover which fingerprint tools hold up at that scale, it’s a different failure mode from what breaks a scraper.
where to go next
- How to bypass Cloudflare 403s with Playwright plus residential proxies for the behavioral and timing side of getting past challenge pages.
- Handling reCAPTCHA v3 in scrapers without dropping IP reputation for what to do once stealth alone stops being enough.
- Debugging 429 errors: rate limits, proxy quality, and behavioural patterns once you’re scaling and need to work out why block rates are climbing.
Full article index at /blog/.
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-07-24.