How Do You Attach a Proxy to nodriver Without It Being Ignored?
A working nodriver proxy setup comes down to one question: does your proxy need a username and password? If it does not, pass --proxy-server=HOST:PORT in browser_args. If it does, call browser.create_context(url, proxy_server="http://user:pass@host:port") and open every page from the tab it returns. Chrome’s proxy flag carries a host and a port only, so credentials written there are dropped and your traffic leaves from your real IP.
📌 TL;DR Executive Summary
- Core Takeaway:
browser_argscarries host:port proxies only;create_context(..., proxy_server=...)handles HTTP and SOCKS5 logins because nodriver runs a local forwarder on 127.0.0.1 that injects the credentials. - Key Risk/Challenge: A broken proxy config fails silently — Chrome renders its own error page, the await still resolves, and your script keeps running from your real connection.
- Recommended Solution: Verify with a dead-port test plus an IP echo endpoint, open pages only through the context tab, silence the nodriver logger, and pin Python 3.10–3.12.
Prerequisites: Pin Python Before You Write Proxy Code
Most “nodriver proxy not working” reports start as an environment problem. Get these five things right before you debug a single line of proxy logic.
- Python 3.10 to 3.12. Version 0.50.3 will not import on Python 3.14 — it raises a
SyntaxErrorfrom a latin-1 byte in its vendoredcdp/network.py. The same file imports cleanly on 3.12. - nodriver 0.50.3 (
pip install nodriver), released 13 May 2026. It drives Chrome over CDP with no WebDriver layer and no ChromeDriver binary to keep in sync. - A Chromium-based browser. The project page on PyPI lists Chromium, Chrome, Edge and Brave. Firefox is out of scope.
- A proxy you can describe in one URL — scheme, host, port, and credentials if it needs them. Keep it in a
.envfile read withpython-dotenvrather than in your source. - A script that is not named
nodriver.py. If it is, your import resolves to your own file and the library never loads.
On the same machine, nodriver launched in 3.1 seconds against 10.4 seconds for SeleniumBase UC mode, and in Ian Patterson’s May 2026 benchmark over 31 live targets it scored 28 OK, 3 gated, 0 blocked, while vanilla Playwright scored 24 OK, 2 gated, 5 blocked. If you are still weighing it against its predecessor, this nodriver vs undetected-chromedriver breakdown covers where each one breaks.
Step 1: Use browser_args Only for Credential-Free Proxies
Chrome’s --proxy-server switch takes a scheme, a host and a port. Nothing else fits. When you write a login into that string, Chrome discards the entire value instead of raising an error, and the browser starts on a direct connection. That is the single most common cause of a proxy that appears to be configured and is not.
import nodriver as uc
PROXY_HOST_PORT = "gate.example.net:8000" # placeholder: your IP-allowlisted gateway
async def main():
browser = await uc.start(
browser_args=[f"--proxy-server={PROXY_HOST_PORT}"],
)
page = await browser.get("https://api.ipify.org")
await page.sleep(3)
print("Exit IP:", (await page.get_content()).strip())
browser.stop()
uc.loop().run_until_complete(main())
The flag applies to the document plus every asset, script and XHR the page triggers, so there is no per-request gap where a tracking pixel slips out over your real connection. Use this route when your provider authenticates by IP allowlist, or when you run a local forwarder yourself and hand Chrome its credential-free address.
Step 2: Attach an Authenticated Proxy With create_context()
Everything with a login goes through create_context(). nodriver starts a local forwarder on 127.0.0.1 on a free port, gives Chrome that credential-free local address, and adds your username and password upstream. Because the forwarder does the authentication, it works for HTTP and SOCKS5 alike.
import nodriver as uc
# Percent-encode anything outside the unreserved set in the password
PROXY = "http://proxyuser:proxysecret@gate.example.net:8000" # placeholder
async def main():
browser = await uc.start()
tab = await browser.create_context(
"https://api.ipify.org",
proxy_server=PROXY,
)
await tab.sleep(3)
print("Exit IP through the proxy:", (await tab.get_content()).strip())
browser.stop()
uc.loop().run_until_complete(main())
Two details matter here. First, a password containing @, : or / must be percent-encoded inside the URL — p@ss becomes p%40ss, or nodriver parses a hostname that does not exist. Second, the proxy_server argument is marked experimental in its docstring, and SOCKS5 failures are commonly reported as ERR_TIMED_OUT. Test the same credentials with curl -x before you blame the Python side.
The upside of contexts is granularity: one browser process, several contexts, a different exit IP in each. That is what makes per-context proxies cheaper than launching several browsers when you need two geographies at once.
Step 3: Open Every Page Through the Returned Tab
Calling browser.get() right after create_context() looks reasonable and quietly bypasses everything you just set up. It opens the page in the browser’s main context, which has no proxy attached. Your proxy only travels into pages opened from the tab object that create_context() returned.
tab = await browser.create_context("https://example.com", proxy_server=PROXY)
await tab.sleep(2)
await tab.get("https://api.ipify.org") # same context, same exit IP
await tab.sleep(2)
print((await tab.get_content()).strip())
# browser.get("https://api.ipify.org") would open in the main context, no proxy
Hold one reference to that tab and navigate with it for the whole session. If two tasks need two different proxies, create two contexts and keep both tabs alive rather than starting a second browser process — context creation is cheap, and Chrome start-up is not.
Step 4: Verify the Proxy Is Actually Being Used
You cannot trust the navigation call as proof. Point the proxy at a closed port, then check the rendered body text. If example.com still returns “Example Domain”, the promise resolved on Chrome’s own proxy error page and your traffic never touched the proxy. If the body is anything else, the proxy path is live.
import nodriver as uc
PROXY = "http://proxyuser:proxysecret@gate.example.net:8000" # placeholder
DEAD = "http://127.0.0.1:9999" # nothing listens here
async def read_body(tab, wait=4):
await tab.sleep(wait)
return await tab.get_content()
async def main():
browser = await uc.start()
dead_tab = await browser.create_context("https://example.com", proxy_server=DEAD)
dead_body = await read_body(dead_tab, 5)
if "Example Domain" in dead_body:
raise SystemExit("Proxy ignored: the page loaded over the real connection")
print("Proxy path is live (Chrome rendered its own error page)")
live_tab = await browser.create_context("https://api.ipify.org", proxy_server=PROXY)
print("Exit IP:", (await read_body(live_tab, 3)).strip())
browser.stop()
uc.loop().run_until_complete(main())
Run both checks, not just one. The failure mode is silent, and published results conflict: one August 2026 test on version 0.50.3 could not get a proxy to take effect at all, while other write-ups report create_context working as documented. Treat a passing IP echo — not a completed navigation — as your only proof. Proxy quality decides what happens next, and the difference between an address a site accepts and one it refuses is bigger than any header your script sets. Our best proxies for antidetect browsers comparison starts with that decision for a reason.
create_context forwarder logs the full proxy URL — credentials included — at info level. Set logging.getLogger("nodriver").setLevel(logging.WARNING) before you launch, or your proxy password lands in CI logs. The forwarder also listens on 127.0.0.1 while the session runs, so any other process on that machine can reach it: treat the box as part of the trust boundary.
Step 5: Silence the Forwarder Logs and Align Timezone and Locale
Chrome takes its clock and locale from your machine, not from the proxy. A session exiting in Frankfurt while Intl.DateTimeFormat().resolvedOptions().timeZone reports a US zone is a mismatch any serious site can read. Fix it in the same place you fix the logging.
import logging
import nodriver as uc
from nodriver import cdp
PROXY = "http://proxyuser:proxysecret@gate.example.net:8000" # placeholder
logging.getLogger("nodriver").setLevel(logging.WARNING)
async def main():
browser = await uc.start()
tab = await browser.create_context("https://example.com", proxy_server=PROXY)
# Align the browser clock and locale with the proxy's exit country.
# Domain and method names follow the CDP spec; check your installed
# version's cdp module for the exact symbol names.
await tab.send(cdp.emulation.set_timezone_override(timezone_id="Europe/Berlin"))
await tab.send(cdp.emulation.set_locale_override(locale="de-DE"))
await tab.get("https://example.com")
browser.stop()
uc.loop().run_until_complete(main())
Because nodriver exposes the whole CDP surface, overrides like these are plain method calls rather than driver plugins. The cost is that you own the coherence: every context needs its own timezone, locale and language headers, kept consistent with the exit country. That manual bookkeeping is exactly what a browser built around per-profile proxies removes — Send.win derives timezone, locale, WebRTC and geolocation from the proxy’s exit IP automatically, in both Sendwin Browser on the desktop and its cloud browser.
Step 6: Survive Sticky-IP Rotation Mid-Run
Residential proxies rotate. A sticky session holds one address for a configured window, then hands you a different one. If the exit IP changes halfway through a job, the site sees two machines sharing one cookie jar — a pattern that ends in a challenge or a logout. Read the exit IP once at session start, then re-check it and rebuild the whole session when it moves.
import nodriver as uc
PROXY = "http://proxyuser:proxysecret@gate.example.net:8000" # placeholder
IP_ECHO = "https://api.ipify.org"
async def run_batch(targets, expected_ip=None, attempts=3):
for attempt in range(1, attempts + 1):
browser = await uc.start()
try:
tab = await browser.create_context(IP_ECHO, proxy_server=PROXY)
await tab.sleep(2)
current_ip = (await tab.get_content()).strip()
if expected_ip and current_ip != expected_ip:
print(f"Sticky IP rotated {expected_ip} to {current_ip}; rebuilding")
expected_ip = current_ip
continue # finally stops the browser, then the loop restarts clean
expected_ip = current_ip
print(f"Session locked to {current_ip}")
for url in targets:
await tab.get(url)
await tab.sleep(3)
print(url, len(await tab.get_content()), "chars")
return expected_ip
finally:
browser.stop()
raise RuntimeError("proxy never held a stable exit IP")
uc.loop().run_until_complete(run_batch(["https://example.com"]))
Rebuilding the browser rather than swapping the proxy on an open context is deliberate: cookies, cache and the forwarder port all belong to the session you are abandoning. If your jobs are long, treat rotation as a scheduling problem and read the longer walkthrough on browser automation proxy rotation before you write retry logic of your own.
uc.start() launches a full Chrome process. But proxy_server is experimental, so if two proxies must be strictly isolated, spend the memory and run two processes.
Common nodriver Proxy Errors and What Fixes Them
Almost every failure in this list is silent, which is why the verification step matters more than any flag you can add.
| Symptom | Likely cause | Fix |
|---|---|---|
| A dead proxy still returns page content | The proxy was never applied; the navigation resolved on Chrome’s error page | Assert on body text, not on the navigation call, and pin your version |
| Proxy works in one call, the site still sees your home IP | browser.get() opened the page in the main context |
Open every page from the tab returned by create_context() |
ERR_TIMED_OUT with a SOCKS5 URL |
proxy_server is experimental; SOCKS5 failures are reported |
Retry the same proxy over HTTP CONNECT, or run your own local forwarder |
| Proxy URL fails to parse or connects to the wrong host | Unencoded characters in the password | Percent-encode: p@ss becomes p%40ss |
SyntaxError at import time |
Python 3.13 or 3.14 reading the vendored cdp/network.py |
Run 3.10–3.12, or patch the offending line in that file |
| Proxy credentials printed in your logs | The forwarder logs the full proxy URL at info level | logging.getLogger("nodriver").setLevel(logging.WARNING) |
ModuleNotFoundError for a package you installed |
Your script is named nodriver.py and shadows it |
Rename the file |
The Full Script
This ties the steps together: config from the environment, logging suppressed, the dead-port check before anything else, an exit-IP read per session, and a session rebuild when the sticky address rotates. Run it on Python 3.10–3.12 and save it as proxy_run.py.
"""nodriver + authenticated proxy, with verification and sticky-IP handling.
Python 3.10-3.12. Save as proxy_run.py, never as nodriver.py.
.env:
NODRIVER_PROXY=http://proxyuser:proxysecret@gate.example.net:8000
"""
import logging
import os
import nodriver as uc
from dotenv import load_dotenv
from nodriver import cdp
load_dotenv()
logging.getLogger("nodriver").setLevel(logging.WARNING)
PROXY = os.environ["NODRIVER_PROXY"]
IP_ECHO = "https://api.ipify.org"
TARGETS = ["https://example.com"]
TIMEZONE = "Europe/Berlin"
async def read_body(tab, wait=3):
await tab.sleep(wait)
return await tab.get_content()
async def assert_proxy_path(browser):
"""A dead proxy must not return real content."""
dead = await browser.create_context("https://example.com", proxy_server="http://127.0.0.1:9999")
body = await read_body(dead, 5)
if "Example Domain" in body:
raise RuntimeError("Proxy ignored: example.com loaded over the real connection")
print("Dead-port check passed: the proxy path is live")
async def run_session(proxy, targets, expected_ip=None, attempts=3):
for attempt in range(1, attempts + 1):
browser = await uc.start()
try:
await assert_proxy_path(browser)
tab = await browser.create_context(IP_ECHO, proxy_server=proxy)
await tab.sleep(2)
current_ip = (await tab.get_content()).strip()
if expected_ip and current_ip != expected_ip:
print(f"Sticky IP rotated {expected_ip} to {current_ip}; rebuilding")
expected_ip = current_ip
continue
expected_ip = current_ip
print(f"Session locked to {current_ip}")
await tab.send(cdp.emulation.set_timezone_override(timezone_id=TIMEZONE))
for url in targets:
await tab.get(url)
html = await read_body(tab)
print(f"{url} returned {len(html)} chars")
return expected_ip
except Exception as exc:
print(f"Attempt {attempt} failed: {exc}")
finally:
browser.stop()
raise SystemExit("All attempts failed")
if __name__ == "__main__":
uc.loop().run_until_complete(run_session(PROXY, TARGETS))
What nodriver Does Not Solve
A correct proxy is necessary and not sufficient. Four limits are worth knowing before you build a pipeline on this library.
- Headless mode leaks.
navigator.userAgentreportsHeadlessChrome, and the repair for that has not been called since version 0.46.1. Headed nodriver failed none of the 31 checks on bot.sannysoft.com; headless failed three. On a display-less machine, the official docs recommend Xvfb over headless mode. - Expert mode makes you easier to spot. It disables web security and origin trials and keeps shadow roots open, which changes what the page can detect. Leave it off for anything that matters.
- Identity is per machine, not per account. nodriver starts a fresh profile each run and cleans up the files it created unless you set
user_data_dir. That is convenient for one job and awkward for twenty accounts that each need a stable identity — the one profile per account approach exists for that reason. - Maintenance and licensing. The last commit and the last release are both 13 May 2026, the oldest pull requests have been open for years, and the repository shipped no test suite or CI at the time of checking. The license is AGPL-3.0, which carries a network clause. Zendriver, a fork, fixes three of the defects measured in September 2026, and SeleniumBase’s CDP Mode is derived from nodriver.
How Send.win Helps With Nodriver Proxy
Send.win is an antidetect browser built for exactly this kind of work — every profile is a clean, isolated identity:
- Isolated profiles – unique fingerprint, separate cookies and storage per profile
- Stealth engine – canvas, WebGL, fonts, and audio spoofed at the engine level
- Desktop app + cloud sessions – native app for Windows, macOS, and Linux, or run profiles in the cloud with no install
- Built-in residential proxies – with automatic timezone, locale, and WebRTC matching
- Team features – share logged-in profiles with teammates without sharing passwords
Try the instant cloud browser demo — no install, no signup — or download the desktop app. The 30-day free trial needs no credit card, and paid plans start at $6.99/month billed annually (see pricing).
None of that stops a proxy from working; it just means you own the upkeep. And no browser library fixes a blocked IP: in a 19 September 2026 test, the same plain requests were answered differently by Zillow, Instagram, Reddit and DuckDuckGo depending on whether they came from a residential or a hosting address — four of thirteen sites changed their answer — while Glassdoor, Amazon and Booking refused both. Budget for proxy quality before you tune fingerprints. Remember too that a real browser costs 10 to 50 times a plain HTTP fetch per page, so use it where the JavaScript actually matters.
🏆 Send.win Verdict
If you are writing nodriver scripts, you already accept that proxy credentials, timezone, locale and profile cleanup are yours to manage. Send.win is the other end of that trade: Sendwin Browser and the cloud browser ship with residential proxies on every plan, spoof canvas, WebGL, audio, fonts and hardware at the engine level, and keep timezone, locale, WebRTC and geolocation aligned with the proxy’s exit IP without a forwarder script. A local Automation API for Selenium, Puppeteer and Playwright is included on the Team plan when you need code rather than clicks, and there is a free cloud preview you can open without installing anything.
Try Send.win free today — 30-day free trial, $0 today, cancel anytime, with 10 isolated profiles, 10 residential proxies and 1 GB of bandwidth included.
Frequently Asked Questions
How do I use an authenticated proxy in nodriver?
Call browser.create_context(url, proxy_server="http://user:pass@host:port") and open pages from the tab it returns. nodriver starts a local forwarder on 127.0.0.1 that adds the login upstream, so the credentials never go into Chrome’s proxy flag. Percent-encode special characters in the password before you build the URL.
Why does my nodriver proxy get ignored silently?
Usually because credentials were written into browser_args, where Chrome’s --proxy-server switch accepts only host and port and drops the whole value. The second cause is opening pages with browser.get() instead of the context tab. Both failures look identical: the script runs, the page renders, and your real IP is used.
Does nodriver support SOCKS5 proxies with username and password?
Yes, through the same proxy_server argument, because the local forwarder performs the authentication rather than Chrome. Be aware that the parameter is marked experimental and that SOCKS5 failures are frequently reported as ERR_TIMED_OUT. Keep an HTTP CONNECT proxy from the same provider handy as a fallback while you test.
How do I check whether nodriver is actually using my proxy?
Run two checks. First, point the proxy at a closed port such as 127.0.0.1:9999 and confirm the page body is not the real content — if “Example Domain” comes back, the proxy was bypassed. Second, load an IP echo endpoint through the context tab and compare the returned address with your own.
What is the difference between browser_args and create_context for proxies?
browser_args=["--proxy-server=HOST:PORT"] sets one proxy for the whole browser and covers the document, its assets, scripts and XHR — but it cannot carry a login. create_context(proxy_server=...) handles authentication and lets each context use a different proxy, at the cost of an experimental feature and a requirement to navigate through the returned tab.
Can I use a different proxy per tab in nodriver?
You can use a different proxy per context, and each tab created inside that context inherits it. Practically, one browser with two contexts gives you two exit IPs for the price of one Chrome process. If the two identities must be strictly isolated, separate browser processes are the safer layout.
Why does nodriver fail to import on Python 3.13 or 3.14?
Version 0.50.3 raises SyntaxError from a latin-1 byte in its vendored cdp/network.py, and the library imports fine on 3.12. That failure is documented on 3.14; on both 3.13 and 3.14 the community workaround is to patch that single line. The simpler fix is to run your automation on Python 3.10–3.12.
Is nodriver still maintained in 2026?
The last release and the last commit were both 13 May 2026, with the PyPI page updated in early October 2026. The repository has long-open pull requests and shipped no test suite or CI at the time of checking. Zendriver, a fork, fixes three of the defects measured in September 2026, and SeleniumBase’s CDP Mode is built on nodriver.