nodriver Review: A Tiny CDP Driver That Won't Import on Python 3.14

Last Updated on August 18, 2026
nodriver Review: A Tiny CDP Driver That Won't Import on Python 3.14
AI Summary
nodriver is a Python browser-automation library from ultrafunkamsterdam, the author behind undetected-chromedriver, and it bills itself as that project's successor. Its architectural break is specifically from Selenium and chromedriver: nodriver talks to Chromium over the Chrome DevTools Protocol (CDP), with no WebDriver binary in the loop, and its API is async. Playwright and Puppeteer are a different comparison class. They are also protocol-driven browser controllers, not WebDriver descendants; the useful differences are API design, packaging, browser provisioning, and compatibility policy. This review inventories nodriver 0.50.3 rather than testing it against live defenses.

nodriver is a Python browser-automation library from ultrafunkamsterdam, the author behind undetected-chromedriver, and it bills itself as that project's successor. Its architectural break is specifically from Selenium and chromedriver: nodriver talks to Chromium over the Chrome DevTools Protocol (CDP), with no WebDriver binary in the loop, and its API is async. Playwright and Puppeteer are a different comparison class. They are also protocol-driven browser controllers, not WebDriver descendants; the useful differences are API design, packaging, browser provisioning, and compatibility policy.

This review inventories nodriver 0.50.3 rather than testing it against live defenses. I measured the installed package, imports, API surface, disk footprint, and license, then launched browsers only against pages served on 127.0.0.1. No live target, anti-bot service, or CAPTCHA was involved. The results describe package behavior and default browser disclosure; they do not establish anti-detection effectiveness.

Three things stood out inside that boundary. First, on Python 3.14 the library does not import at all — a single stray byte kills the whole package before you can call anything. Second, for a driver in this category it is remarkably small: three declared dependencies plus one resolved transitive package, and about 17 MB in the measured environment. Third, on my own page, the only property where nodriver visibly differs from stock Playwright and stock Puppeteer is one boolean — and the headless user-agent string still says HeadlessChrome in all three. The rest of the surprises are in the license.

The Python 3.14 import break

Start with the finding that will bite you first, because it happens before any of your code runs. On Python 3.14, a plain import nodriver fails outright:

File ".../nodriver/cdp/network.py", line 1345
    #: JSON (±Inf).
             ^
SyntaxError: Non-UTF-8 code starting with '\xb1' on line 1345, but no encoding declared; see PEP 263

The failing file is auto-generated cdp/network.py (its header says # DO NOT EDIT THIS FILE!). It contains one non-UTF-8 byte, 0xb1, for the ± in the comment #: JSON (±Inf)., and has no source-encoding declaration. The module loads through nodriver/__init__ → cdp/__init__ → network, so the parse failure aborts the import. A package scan found no other non-UTF-8 source file.

The version boundary needs careful wording. Python 3.14.2 rejects the file, while Python 3.12.13 imports it unpatched. The installed network.py files are byte-identical in those environments (SHA-256 ef755f41800d4efb593736f8b55b331bba68eec373ed62ece433e66e4b491cd6), so different source artifacts do not explain the result. This review did not isolate the exact CPython tokenizer change responsible, and Python 3.13 was not tested. It reports the two measured interpreter results rather than claiming every earlier Python behaves like 3.12.

No conclusion about Python 3.13 follows from those two measured endpoints.

This is a reproduction, not a discovery. The same Python 3.14 traceback is recorded in nodriver issue #35, with a proposed fix in pull request #36. Version 0.50.3 still contains the byte. PyPI metadata lists classifiers through Python 3.13 and does not claim 3.14 support.

The workaround is as small as the bug: use a tested interpreter version or re-encode that one file to UTF-8, as pull request #36 proposes. After the re-encode, the package imports and introspects on 3.14 with no second blocker surfacing. The clean 3.12.13 run below verifies one unpatched path; Python 3.13 was not tested in this review.

On Python 3.12.13, import nodriver succeeds with no patch. Every browser-driving number later in this review comes from that install. The footprint and import-timing figures use the one-byte-re-encoded copy on 3.14 and are labeled accordingly. Python 3.13 remains untested here; one 3.12 result is not coverage of the entire earlier range.

If you run modern interpreters by default — and plenty of teams jump on a new Python fast — this is a real, if trivially fixable, wall. Know it's there so you don't lose an afternoon to a SyntaxError in a file you never wrote.

What nodriver actually is, under the hood

The one-line description ("CDP-native, no webdriver") sounds like marketing until you look at what ships in the wheel. nodriver bundles its own complete set of DevTools Protocol bindings: the nodriver.cdp package contains 57 protocol-domain modules — accessibility, dom, network, page, fetch, runtime, target, storage, input, emulation, and so on down the list. That module count is the mechanism behind the "direct CDP, no Selenium" claim. Instead of shelling out to a chromedriver executable that speaks WebDriver and translates for you, nodriver generates Python objects for the CDP domains and speaks the protocol itself over a WebSocket. cdp/network.py — the file with the bad byte — is one of those 57 auto-generated modules, which is why the break lives in generated code nobody hand-edits.

On top of that protocol layer sits a friendlier object model. The Tab object exposes 62 public methods, and the finding surface is broader than most drivers bother with: text matching via find() and find_all(), CSS via select() and select_all(), and a native xpath() entry point. XPath, CSS, and text search on the same object is genuinely convenient; other libraries sometimes make you drop to evaluate() for XPath. The Config constructor exposes user_data_dir, headless, browser_executable_path, browser_args, sandbox, lang (default 'en-US'), host, port, expert, plus **kwargs. During this API inventory, constructing Config(headless=True) produced 16 Chromium launch flags, including --no-first-run, --no-default-browser-check, --remote-allow-origins=*, and --homepage=about:blank. I did not call start() in this introspection step. The browser tests below were a separate run.

System diagram: Direct CDP Control Path

The library does ship an anti-detection-oriented API surface — methods whose existence I confirmed but whose behavior I did not exercise against any target. I'm noting that once and moving on, because its effect against any real service is exactly the thing I chose not to test. What I'll say about it neutrally: nodriver's naming is more restrained than some stealth-branded peers. Its story is architectural — CDP-native, fresh profile per run — rather than a wall of detect_and_bypass-style method names. Read that as an observation about API design, not a claim about outcomes.

Those counts came from importing the package and using Python's own introspection — inspect, module walking, attribute counting. No site was touched. If you want to re-run it, the numbers come straight out of the package; they're not vibes.

What it announces about itself

Measured results chart: Browser disclosure fields in the tested stacks

Here's a question you can answer without going anywhere near a real defense: when nodriver drives a browser, what does that browser volunteer about itself to the page it's looking at? I wrote a page that reads the obvious things — navigator.webdriver, the user-agent, platform, languages, plugin and hardware counts, the shape of window.chrome, what the Permissions API says, window and screen geometry — served it on 127.0.0.1, and pointed four stacks at it: nodriver, Botasaurus, and stock Playwright and stock Puppeteer as controls. All four drove the same Chrome build (Chrome for Testing 151.0.7922.10), so anything that differs is the library and not the browser. Headless and headed, three runs each. Every value below held across all three.

StackModenavigator.webdriverUser-agent tokennavigator.languages
nodriver 0.50.3headlessfalseHeadlessChrome/151.0.0.0["en-US"]
nodriver 0.50.3headedfalseChrome/151.0.0.0["en-US"]
Botasaurus 4.0.92headless / headedfalseHeadlessChrome/151 / Chrome/151["en-US"]
Playwright 1.56.0headless / headedtrueHeadlessChrome/151 / Chrome/151["en-US","en"]
Puppeteer 24.16.0headless / headedtrueHeadlessChrome/151 / Chrome/151["en-US"]

The clearest difference is that single boolean. Under these default launch configurations, nodriver reports navigator.webdriver as false while stock Playwright and Puppeteer report true, in both headless and headed runs. The test identifies a configuration-level difference; it does not prove that the value follows merely from the absence of a WebDriver binary.

In all four stacks the property is still the browser's native getter on Navigator.prototype — function get webdriver() { [native code] } — never an own-property on the instance or a replaced function. The property was not rewritten by page JavaScript after load. The test did not enumerate the launch argument or source path responsible for the differing value.

The second detail is the one that qualifies the marketing. In headless mode, nodriver's user-agent still announces HeadlessChrome/151.0.0.0 — identical to stock Puppeteer, identical to stock Playwright. Run headed and it becomes Chrome/151.0.0.0, again identically. If you assumed an anti-detection library masks the most famous self-identifying string in browser automation by default, it does not. You'd be setting that yourself.

Nearly everything else was the same across all four stacks, which is worth saying plainly because it narrows the story — every property below read the same on nodriver, Botasaurus, Playwright and Puppeteer:

PropertyValue, identical on all four stacks
platformMacIntel
vendorGoogle Inc.
Pluginsfive
MIME typestwo
pdfViewerEnabledtrue
Logical corestwelve
Reported device memory16 GB
Touch pointszero
window.chromepresent, with app/csi/loadTimes and no runtime
WebGL renderer stringidentical across all four

The old chestnut where the Permissions API and Notification.permission contradict each other didn't appear anywhere — all four reported default and prompt in agreement. I also scanned document and window for the cdc_-style leftovers that older WebDriver stacks were known for: empty in all four.

The one place nodriver looks more like a bare automated browser than a control does is window geometry. Headless nodriver reports outerWidth/outerHeight of 0Ă—0 on an 800Ă—600 screen; headless Playwright reports 1280Ă—720 because it sets a viewport for you. Puppeteer sits with nodriver at 0Ă—0. That's a default-configuration difference, not a capability one, and it's yours to change.

One last thing worth knowing before you deploy: nodriver doesn't ship a browser, so out of the box it uses whatever Chrome your machine already has. On mine that meant it auto-detected /Applications/Google Chrome.app — Chrome 150.0.7871.187 — and the user-agent it disclosed was that version, not a pinned one. Your fleet's disclosed browser version is whatever your fleet happens to have installed.

Say what this is and what it isn't. It's a record of what an automated stack discloses when nobody has asked it to hide — useful if you're on the defending side, useful if you want to know what your own tooling broadcasts. It is not a measure of whether any of that matters to any particular service. I did not test that, and no row above should be read as implying an outcome.

Can it actually get content off a page?

Announcing yourself is one thing; returning the right HTML is the job. I ran nodriver against the same three-content-class fixture the rest of this benchmark repo uses, so the numbers line up with every other tool measured here. The page carries three things: A, a static link whose marker is a literal in the served bytes; B, a node built by an inline script during parse, with its marker and URL assembled from fragments so that only executing the JavaScript reveals them; and C, a node injected 800 ms after the load event, assembled the same way. Class C is the adversarial one — a read taken at the load event cannot see it.

StackDefault readWith an explicit wait
nodriver 0.50.32 of 3 (A + B, misses C)3 of 3
Botasaurus 4.0.922 of 33 of 3
Playwright 1.56.02 of 33 of 3
Puppeteer 24.16.02 of 33 of 3

nodriver lands exactly where the heavyweights land. browser.get() followed straight by tab.get_content() is an at-load snapshot: it renders JavaScript properly — class B proves that, since class B exists nowhere in the served bytes — but it misses anything injected after load. Add tab.select("#delayed-injected") and you get all three. Same footgun, same fix, as Playwright and Puppeteer. Stable across three reps and three separate runs of the whole suite, with no flakes.

I swept the injection delay to find where the default read gives up. nodriver stops seeing class C once injection lands 100 ms or more after load — the same boundary as both stock controls. (Botasaurus is the outlier here, and it's the one genuinely interesting difference between the two anti-detect libraries: its get() blocks for complete page load by default, so its default read still catches injection at 300 ms. It pays about 250 ms per navigation for that.)

The wait itself has a quirk worth budgeting for. In this sweep, nodriver's tab.select() landed in coarse polling buckets rather than tracking the injection delay closely:

Class-C delay0 ms100 ms400 ms800 ms1500 ms
nodriver select()124–152 ms1132–11411128–11292132–21772138–2150
Puppeteer waitForSelector113–129 ms203–216512–516911–9191608–1611

In this fixture, a 100 ms injection led to roughly 1.1 seconds for select(). The installed loop performs await self and then await self.sleep(0.5) after a miss; the measured combined cycle landed near one second here, but await self is not established as a universal fixed-duration sleep. Every tested delayed node was found. Puppeteer's waiter tracked these delays more closely. Many serial waits could compound the difference, although this review did not benchmark a thirty-selector production page.

Launching is the other place the async-and-lean framing meets reality. Bringing up the browser put nodriver in the same neighborhood as Botasaurus and Puppeteer, and well behind Playwright:

StackBrowser launch, across runs
nodriver 0.50.3910–1583 ms
Botasaurus 4.0.92986–1151 ms
Puppeteer 24.16.0969–1008 ms
Playwright 1.56.0282–365 ms

Once running, nodriver's navigate-and-read is the quickest of the four at 119–129 ms. Lean at the wheel, ordinary at the ignition.

Install and footprint: the genuinely good part

Here is where nodriver earns its "focused" label, and it's a benign, verifiable install fact rather than anything about scraping. This is what a clean pip install nodriver resolves to:

Install factnodriver 0.50.3
Declared direct runtime dependenciesthree — websockets, mss, deprecated
Resolved transitive dependencywrapt (via deprecated)
Measured site-packages totalabout 17.2 MB across 6 dist-info directories, including the environment's pip
nodriver's own share of that3.7 MB
numpy / lxmlneither
Browser binary at install timenone pulled down

For a category that routinely drags in a rendering stack, that's a light touch.

The distinction between direct and transitive packages matters for maintenance. nodriver's metadata asks for mss, websockets, and deprecated; wrapt arrives because deprecated requires it. The sixth dist-info directory in the 17.2 MB environment is pip, which was already part of that virtual environment. So "17.2 MB across six distributions" describes the measured environment, while "four added runtime packages" describes what the install resolved. They are related counts, not interchangeable ones.

The contrast that makes the number mean something: on the same machine, the sibling framework Botasaurus weighs 122.3 MB across 44 packages — roughly seven times the disk footprint. That's the difference between a focused CDP driver and a batteries-included framework, and it cuts both ways. nodriver gives you a thin, legible dependency tree you can actually audit; Botasaurus gives you more out of the box and charges you disk and dependency surface for it. Neither is "better" in the abstract — it depends on whether you want a driver or a framework — but if you value a small, inspectable install, nodriver is unusually clean for what it does.

The small disk footprint does not imply a small import. On the byte-fixed Python 3.14 copy, import nodriver took about 158 ms (median across fresh subprocess imports, roughly 151–199 ms). The unpatched 3.14 import fails, so no timing is reported for it. The 57 CDP domain modules load eagerly, and post-import resident memory was 31.5–31.9 MB before any Chrome process existed. A launched browser would add much more and was not included in this memory measurement.

Two caveats stapled to those particular numbers. They're from one machine — macOS arm64 — and the footprint and import-timing figures were taken on Python 3.14 against the one-byte-patched copy, because the unpatched package doesn't import on that interpreter at all. On a supported Python you don't need the patch: I confirmed a clean unpatched import on 3.12.13, which is where all the browser measurements come from. And a real Chrome, Chromium, Edge, or Brave binary is still required at runtime — nodriver drives an existing browser, it doesn't ship one, so that install cost lives outside the 17 MB, and as the disclosure section showed, whichever Chrome your machine happens to have is the version your traffic announces.

The license is the real adoption decision

System diagram: The license is the real adoption decision

Most reviews of a free tool treat "it's open source" as the end of the licensing conversation. For nodriver it's the start of one, because the license is AGPL-3.0 — confirmed both in the wheel's LICENSE.txt and in the repo's spdx_id. That is a strong network-copyleft license, and it is a materially different commitment from what the neighbors ask.

The peers most people weigh nodriver against are permissive, which is what makes the contrast concrete:

ToolLicenseIf you run a modified copy as a network service
nodriverAGPL-3.0Section 13 can require an operator to offer the corresponding source of its modified version to remote users
PlaywrightApache-2.0no equivalent network-copyleft clause
PuppeteerApache-2.0as above
BotasaurusMITas above

The scope matters. AGPL Section 13 addresses a modified version of the covered program used for remote network interaction. This review does not decide whether separate surrounding service code forms part of the covered work, nor does it resolve internal-use or corporate-boundary edge cases. If a hosted product modifies nodriver, review the license text and architecture with counsel. This is a technical adoption flag, not legal advice.

I'm not editorializing on whether AGPL is good or bad — copyleft is a legitimate choice and plenty of serious projects use it. I'm flagging that "nodriver is free and open source" is true and incomplete. The obligation is real, it's different from the permissive default in this corner of the ecosystem, and it belongs in the decision rather than getting flattened into "free." (PyPI, for what it's worth, ships no license classifier at all; the AGPL text is in the wheel and the SPDX id is in the repo, so don't rely on the package index to surface this for you.)

The metadata, dated

Point-in-time repo and package numbers, straight from the GitHub API and PyPI:

FactValue on July 14, 2026
Stars4,511
Forks422
Open issues14
CreatedFebruary 2024
Last pushedMay 2026
Latest PyPI release0.50.3
Wheelpure-Python py3-none-any
requires-python>=3.9
Python classifiers3.7–3.13

The observable maintenance signals are mixed: the repository was pushed in May 2026, while the latest measured package still contained the Python 3.14 import problem and the proposed fix had not shipped by the research date. Stars and open-issue counts do not settle whether that cadence meets your maintenance threshold.

Pros and cons

Pros:

  • Small, legible install: three declared dependencies plus transitive wrapt, ~17.2 MB across six dist-info directories in the measured environment (one is pip), no numpy/lxml, and no browser download at install time.
  • Genuinely CDP-native: bundles its own 57-module DevTools Protocol bindings and talks the protocol directly, with no chromedriver/Selenium binary in the loop.
  • Broad element-finding surface on Tab (62 public methods) with first-class XPath, CSS, and text search — no dropping to raw evaluate() for XPath.
  • Async by design, fresh profile per run, and a clean Config surface for the common knobs (headless, executable path, args, lang, ports).
  • Pulls content off a JavaScript-built page as competently as the heavyweights: 2 of 3 content classes on a default read, 3 of 3 with an explicit wait — identical to stock Playwright and Puppeteer on the same fixture, stable across three runs. Navigate-and-read was the quickest of the four at 119–129 ms.
  • Reports navigator.webdriver as false by default where both stock controls report true, without patching the property — the descriptor stays the browser's own native getter.
  • A clear architectural identity as undetected-chromedriver's CDP-native successor.

Cons:

  • Does not import on Python 3.14 out of the box — a one-byte non-UTF-8 file (cdp/network.py) throws SyntaxError at import. Reproduced against open issue #35, still unfixed in 0.50.3. Pin ≤3.13 (verified clean on 3.12.13) or re-encode the file.
  • AGPL-3.0 is a real adoption consideration for anyone running a modified copy as a network service — stricter than the Apache/MIT peers.
  • The tiny disk footprint doesn't mean a tiny import: ~158 ms cold-start and ~31.5 MB resident before any browser exists, because all 57 CDP modules load eagerly.
  • tab.select() polls on a half-second back-off, so short waits round up — a 100 ms wait costs ~1.1 s where Puppeteer's waiter costs ~210 ms. Correct every time, but it adds up across many small waits.
  • Headless runs still announce HeadlessChrome in the user-agent by default, exactly like the stock controls; nothing about the default configuration hides the most obvious self-identifying string.
  • Still needs a real Chrome/Chromium/Edge/Brave binary at runtime; the light pip install is only half the dependency story, and whichever Chrome your machine has is the version your traffic discloses.
  • Effectiveness against any anti-bot system is unverified here — the whole stealth premise is untested by design.

What I did not test, and therefore can't speak to: per-tab memory, CDP round-trip latency, profile handling, throughput at scale, Linux or Windows, Python 3.13 specifically, and — the big one — real-world anti-bot effectiveness against any live service. Every browser in this review only ever talked to a fixture on 127.0.0.1. All numbers are one machine (macOS arm64); the browser-driving figures are Python 3.12.13 with no patch applied, and the older footprint and import-timing figures are Python 3.14 on the one-byte-patched copy.

I also did not run a browser-upgrade matrix, so compatibility across future Chrome releases remains an operational check for the adopter, not a result of this review.

Who it's for, and who should skip it

nodriver fits if you want a lean, async, CDP-native driver for a live Chromium and are comfortable owning the browser, updates, and runtime. The small dependency tree is easier to audit, and the CDP-first design suits protocol-level control. Container suitability remains untested: this review did not exercise Linux images, browser installation, shared libraries, sandboxing, or process cleanup.

Two groups should look elsewhere. If you're on Python 3.14 and unwilling to pin your interpreter or patch a vendored file, wait for the fix to ship — the import break is a hard stop today. And if AGPL-3.0 is a problem for how you plan to ship — a hosted service with private modifications — the license alone is enough reason to weigh a permissively licensed alternative before you build on this one. Neither is a knock on the code; both are constraints you'd rather learn now than during a compliance review.

Also skip it if what you actually need is data, not a browser you drive by hand. nodriver hands you a scriptable tab and 62 methods; turning a rendered page into clean, structured records is still your code to write. That's a different job, and it's the seam where a managed API comes in.

Alternatives, and where Thunderbit fits

The honest framing first: nodriver is free, AGPL, and self-hosted. You run the browser, you manage the updates, you own the runtime and every failure in it. For a developer who wants exactly that control, no managed service competes on price with a library you already have.

Within open source, compare by job rather than by logo. If you're weighing real-browser drivers, our Playwright and Puppeteer comparison covers the two obvious heavyweights nodriver stands next to in shape. Scrapling is the closest Python neighbor on the stealth-oriented axis if that's your reason for looking. For LLM-ready output rather than raw browser control, Crawl4AI renders pages and hands back Markdown, and Scrapy remains the framework of record for large browserless crawls. If you're comparing several of these at once, the open-source scraper roundup lays the categories side by side.

Disclosure: this article is published by Thunderbit. Thunderbit is a managed extraction service, so the comparison is task-based rather than architectural. nodriver gives you a browser-control layer that you host and program; Thunderbit handles rendering and returns page content or schema-shaped records as a service. Use nodriver when protocol-level browser control and self-hosting are requirements. Consider a managed extractor when the output record and operational handoff matter more than owning the browser. Mutable endpoint, credit, and batch-limit details belong on the pricing page, not inside a library benchmark.

The trade-off is where the work lives: nodriver keeps browser management and runtime upkeep on your side without a per-request service fee; a managed API takes on that layer and charges for calls.

Try Thunderbit for Web Data Extraction

Verdict

Use nodriver if you want a small, async, CDP-native Chromium driver, have confirmed it on your Python version (3.12.13 imported clean here), and have reviewed AGPL-3.0 for how you ship. Its resolved runtime graph added four packages, the measured environment occupied ~17 MB, and the library ships 57 CDP-domain modules with a 62-method Tab surface and native XPath. On this fixture it returned 2 of 3 content classes by default and 3 of 3 with a wait, matching stock Playwright and Puppeteer.

Size the caveats honestly, though. On Python 3.14 it doesn't import at all until you fix one non-UTF-8 byte — a documented, still-open issue, not a mystery, but a hard stop the day you hit it. The license is AGPL-3.0, which is a genuine decision for anyone running a modified copy as a service, not a formality. The small install doesn't buy a small import, since all those CDP modules load up front, and select()'s half-second poll makes short waits cost about a second each. On the default-disclosure question I could actually answer, the picture is narrower than the marketing implies: one boolean differs from stock Puppeteer, the headless user-agent still says HeadlessChrome, and every other property I measured was identical across all four stacks. And the entire anti-detection premise — the reason a lot of people find nodriver in the first place — is something I deliberately did not test. I inventoried the library and drove it against a page on my own machine rather than pitting it against live defenses, and I'd rather tell you that plainly than hand you a bypass claim I can't stand behind. On the questions I could answer, nodriver is a well-built, unusually lean driver with two sharp edges — a Python-version wall and a copyleft license — that you want to see coming.

Try Thunderbit for Web Data Extraction Get Started Free

FAQs

Why does import nodriver fail on Python 3.14? Because cdp/network.py contains a non-UTF-8 ± byte without a source-encoding declaration. Python 3.14.2 rejects the file and aborts the transitive import; Python 3.12.13 imports the byte-identical file unpatched. This review did not isolate the exact interpreter change and did not test Python 3.13. The upstream trail is nodriver issue #35 and pull request #36. Use a version you have tested, or re-encode the file to UTF-8.

What does "CDP-native, no webdriver" buy you, and what does it cost to install? nodriver bundles 57 DevTools Protocol domain modules and talks CDP over a WebSocket instead of invoking chromedriver through Selenium. Tab exposes 62 methods, including native XPath. Package metadata declares three runtime requirements (websockets, mss, deprecated); resolving them adds wrapt. The measured environment used about 17.2 MB across six dist-info directories, including pip, with no numpy, lxml, or downloaded browser binary. Two caveats: the patched-copy import took ~158 ms because all 57 CDP modules load eagerly, and a Chrome-family browser is still required separately.

Does the AGPL-3.0 license matter for my project? It depends on how you ship. AGPL-3.0 is a network-copyleft license: if you run a modified version of nodriver as a service other people use, you're obliged to offer them that modified source. For a personal script or an internal tool you never expose, it's a non-issue. For a hosted commercial product built on a patched nodriver, it's a real question to raise with whoever owns compliance — and it's stricter than the Apache-2.0 and MIT licenses on comparable tools.

What does nodriver expose about itself — and does that mean it beats Cloudflare? The first half is measured; the second half is not, and the difference matters. On a page I served from 127.0.0.1, driving the same Chrome build as the controls: navigator.webdriver comes back false, where stock Playwright and stock Puppeteer both report true. That value is set at browser-launch time, not by patching the property — the descriptor is still Chrome's own native getter. Beyond that boolean, almost everything matched the controls exactly: same platform string, five plugins, twelve cores, 16 GB reported device memory, same window.chrome shape, no Permissions-API contradiction, and no cdc_-style leftovers on document or window. Headless runs still announce HeadlessChrome/151.0.0.0 in the user-agent, same as both controls — that isn't masked for you. What none of that tells you is whether any of it works against a real anti-bot service. I never pointed nodriver at a live site, never contacted an anti-bot service, and never touched a CAPTCHA; that was out of scope by design. The disclosure table above says what the stack announces. It says nothing about who is listening, or what they do about it.

Does nodriver handle JavaScript-rendered content properly? Yes, with the usual caveat about when you read. On a fixture with three content classes, a default browser.get() + tab.get_content() returned 2 of 3 — it executes JavaScript correctly (the sync-injected class exists nowhere in the served bytes and still came back), but it reads at the load event, so it misses anything injected afterward. Adding tab.select("#delayed-injected") returned 3 of 3. That's identical to stock Playwright and stock Puppeteer on the same page. Budget for one quirk: select() polls on a half-second back-off, so a 100 ms wait costs about 1.1 seconds.

Ke
Ke
CTO at Thunderbit | Senior Data Scientist & ML Expert With nearly a decade of experience in machine learning and data science, Ke Shen is a Columbia University alumnus and former Senior Data Scientist at Walmart Labs. With deep, peer-recognized expertise in Python, R, Java, and Statistics, he shares battle-tested insights on taking complex AI algorithms from theory to production-grade architecture.
Table of Contents
Thunderbit · AI web data agent

Extract data from any page in 1 click

Trusted by 250,000+ users
free plan available
From webpage to spreadsheet
Describe what you need — Thunderbit's AI Agent scrapes it and exports to Excel, Google Sheets, Airtable, or Notion. Free to start.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week