You point Selenium at a proxy that needs a username and password, and Chrome pops up a sign-in box that WebDriver cannot click. Or it shows an error page. Or the script just sits there until the page-load timeout. None of this is your fault; it is how the browsers are built.
Below are four ways to run Selenium through an authenticated proxy that work in 2026, each tested with Selenium 4.49 against Chrome 154 and Firefox 156, plus the popular one to skip.
Why can’t Selenium send a proxy username and password?
Because neither browser takes one at launch. Chrome’s --proxy-server flag has a host and a port and nowhere to put credentials, and Firefox’s proxy preferences have no password field either. Selenium’s proxy options just fill in those same settings. Writing the credentials into the URL looks like it should work, and does not:
options.add_argument("--proxy-server=http://USER:[email protected]:8000")In our test Chrome loaded an error page instead. When the proxy asks for a password, both browsers ask the human at the keyboard, and in an automated session there is not one. So you either remove the question or answer it some other way.
| Approach | Browsers | Extra moving parts |
|---|---|---|
| IP allowlist | Any | None, if your IP is fixed |
| Local forwarding proxy | Any | One small process |
| Generated extension | Chrome | An extension folder and BiDi |
| BiDi auth handler | Chrome, Firefox | None; a Chrome quirk to know |
Option 1: allowlist your IP and skip the password
Every line we sell supports an IP allowlist as well as a username and password. Add the public IP of the machine that runs Selenium in the dashboard, and the proxy stops asking. There is nothing left to authenticate, so the plain proxy options work in both browsers:
from selenium import webdriver
from selenium.webdriver.common.proxy import Proxy, ProxyType
PROXY = "resi.proxymonkey.io:8000"
options = webdriver.ChromeOptions() # or webdriver.FirefoxOptions()
options.proxy = Proxy({"proxyType": ProxyType.MANUAL, "httpProxy": PROXY, "sslProxy": PROXY})
driver = webdriver.Chrome(options=options)
driver.get("https://httpbin.org/ip")
print(driver.find_element("tag name", "body").text)
driver.quit()This is the best option when it fits, and it fits servers with a fixed IP. It does not fit a laptop, a home connection or a CI runner whose address changes, and anyone else behind the same public IP can use the proxy too.
Option 2: a local forwarding proxy that adds the credentials
If the browser cannot send a password, put something next to it that can. This script listens on your machine, adds a Proxy-Authorization header to each request, and passes everything on to the gateway. The browser sees a proxy with no password; the gateway sees a proxy client with the right one.
# relay.py
import asyncio
import base64
LISTEN = ("127.0.0.1", 8899)
UPSTREAM = ("resi.proxymonkey.io", 8000)
USER, PASS = "USER", "PASS"
AUTH = b"Proxy-Authorization: Basic " + base64.b64encode(f"{USER}:{PASS}".encode()) + b"\r\n"
DROP = (b"proxy-authorization:", b"proxy-connection:", b"connection:")
async def pipe(reader, writer):
try:
while data := await reader.read(65536):
writer.write(data)
await writer.drain()
except ConnectionError:
pass
finally:
writer.close()
async def handle(client_reader, client_writer):
try:
head = await client_reader.readuntil(b"\r\n\r\n")
except (asyncio.IncompleteReadError, asyncio.LimitOverrunError):
client_writer.close()
return
first, *headers = head[:-4].split(b"\r\n")
kept = [h for h in headers if not h.lower().startswith(DROP)]
if not first.startswith(b"CONNECT "):
kept.append(b"Connection: close")
head = b"\r\n".join([first, *kept]) + b"\r\n" + AUTH + b"\r\n"
upstream_reader, upstream_writer = await asyncio.open_connection(*UPSTREAM)
upstream_writer.write(head)
await asyncio.gather(
pipe(client_reader, upstream_writer),
pipe(upstream_reader, client_writer),
)
async def main():
server = await asyncio.start_server(handle, *LISTEN)
print(f"forwarding {LISTEN[0]}:{LISTEN[1]} -> {UPSTREAM[0]}:{UPSTREAM[1]}")
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())Run python relay.py, then point the browser at it with the same code as option 1 and PROXY = "127.0.0.1:8899". HTTPS sites use a CONNECT tunnel, which carries the header once and then just relays encrypted bytes, so the script never sees your traffic in the clear. Plain HTTP requests get Connection: close, so each one arrives on its own connection with its own header.
Rotation still works the way it does without the relay: each new connection to the gateway gets a new exit IP. Keep the listener on 127.0.0.1. Bound to a public interface, it is an open proxy running on your credentials.
Prefer an off-the-shelf tool? pproxy does the same in one line (the credentials go after the #):
pip install pproxy
pproxy -l http://127.0.0.1:8899 -r "http://resi.proxymonkey.io:8000#USER:PASS"Option 3: a Chrome extension generated on the fly (Manifest V3)
Extensions can still answer proxy logins under Manifest V3. The webRequestBlocking permission is now limited to policy-installed extensions, but Chrome’s documentation keeps a separate webRequestAuthProvider permission for exactly this, and with it a blocking onAuthRequired listener works. This function writes such an extension to a temporary folder:
import json
import tempfile
from pathlib import Path
def proxy_extension(host, port, user, password):
folder = Path(tempfile.mkdtemp(prefix="proxy-auth-"))
manifest = {
"manifest_version": 3,
"name": "proxy-auth",
"version": "1.0",
"permissions": ["proxy", "webRequest", "webRequestAuthProvider"],
"host_permissions": ["<all_urls>"],
"background": {"service_worker": "background.js"},
}
background = f"""
chrome.proxy.settings.set({{
value: {{
mode: "fixed_servers",
rules: {{ singleProxy: {{ scheme: "http", host: {json.dumps(host)}, port: {int(port)} }} }},
}},
scope: "regular",
}});
chrome.webRequest.onAuthRequired.addListener(
(details) => details.isProxy
? {{ authCredentials: {{ username: {json.dumps(user)}, password: {json.dumps(password)} }} }}
: {{}},
{{ urls: ["<all_urls>"] }},
["blocking"],
);
"""
(folder / "manifest.json").write_text(json.dumps(manifest, indent=2))
(folder / "background.js").write_text(background)
return str(folder)The isProxy check means it only answers the proxy, never a site’s own login box. Loading it is where the old recipes break: the --load-extension flag they use was silently ignored by branded Chrome 154 in our test, and traffic went direct. Selenium’s BiDi install command works instead:
from selenium import webdriver
options = webdriver.ChromeOptions()
options.enable_webextensions = True
options.enable_bidi = True
driver = webdriver.Chrome(options=options)
driver.webextension.install(path=proxy_extension("resi.proxymonkey.io", 8000, "USER", "PASS"))
driver.get("https://httpbin.org/ip")enable_webextensions adds two Chrome flags, one of which connects the driver over a pipe instead of a port and turns off some DevTools features. If you rely on those, pick another option.
Option 4: Selenium 4 BiDi and add_auth_handler
WebDriver BiDi, the newer protocol Selenium 4 speaks next to the classic one, can answer authentication challenges, proxy ones included. It is the least code of the four:
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--proxy-server=http://resi.proxymonkey.io:8000")
options.enable_bidi = True
driver = webdriver.Chrome(options=options)
driver.set_page_load_timeout(30)
driver.network.add_auth_handler("USER", "PASS")
driver.browsing_context.navigate(
context=driver.current_window_handle,
url="https://httpbin.org/ip",
wait="complete",
)
driver.get("https://httpbin.org/headers")Note the first page. With ChromeDriver, a plain driver.get that hits the proxy’s challenge hung in our tests until it timed out: the classic command waits for the page, and the answer to the challenge waits behind the classic command. Doing the first navigation over BiDi lets the answer through, and after that Chrome reuses the credentials, so ordinary driver.get calls work. Set a page-load timeout regardless, so a challenge that does slip through fails in 30 seconds instead of hanging a worker.
Firefox had no such quirk. This worked from the first page:
from selenium import webdriver
from selenium.webdriver.common.proxy import Proxy, ProxyType
PROXY = "resi.proxymonkey.io:8000"
options = webdriver.FirefoxOptions()
options.proxy = Proxy({"proxyType": ProxyType.MANUAL, "httpProxy": PROXY, "sslProxy": PROXY})
options.enable_bidi = True
driver = webdriver.Firefox(options=options)
driver.network.add_auth_handler("USER", "PASS")
driver.get("https://httpbin.org/ip")The Chrome DevTools Protocol’s Fetch.authRequired is the older, Chrome-only way to do this. BiDi is the standard replacement, so there is little reason to hand-roll it in 2026.
How do I check Selenium is really using the proxy?
Load https://httpbin.org/ip and compare the address with your own. Do not skip this: in our tests, a proxy setting the browser quietly ignored did not fail, it just loaded the page directly from the machine’s own IP. A page that loads proves nothing.
Then think about what one page means on a rotating line. A single page load is dozens of requests over several connections, and on the residential gateway each new connection can leave from a different address. That is fine for public pages. For a login, a cart or anything with a session, use a sticky session (a dashboard setting) or a static ISP address, and one driver per address. The rotating vs sticky guide has the patterns.
When it still fails
- A sign-in box, or a hang until timeout: nothing answered the challenge. The extension did not load, the relay is not running, or the Chrome BiDi first-page step was skipped.
ERR_TUNNEL_CONNECTION_FAILEDor a 407: the proxy rejected the credentials. Copy them again from the dashboard, and check the allowlist still holds your current IP.ERR_PROXY_CONNECTION_FAILED: wrong host or port, or the relay is not listening where the browser points.
What about selenium-wire?
Skip it. It used to be the standard answer, and it is archived: the README opens with “Selenium Wire is no longer being maintained”, the last release (5.1.0) shipped in October 2022 and the last commit landed in January 2024. It also worked by running its own interception proxy with its own certificate authority, decrypting every HTTPS page you loaded. An unmaintained package in that position is a bad trade when four maintained options exist.
Which one should you use?
- Fixed server IP: the allowlist. Nothing to run, nothing to break.
- Anything else, any browser: the local forwarding proxy. It is dull in the best way.
- Firefox, or Chrome and you do not mind the first-page step: BiDi, with no extra process at all.
- You already ship a Chrome extension: add the auth listener to it.
If you can switch tools, Playwright takes a proxy username and password as plain launch options and none of this page applies; the Playwright setup and the Playwright proxy guide have the code. Still stuck on a 407? That page walks through the usual causes, and the Selenium setup page covers timeouts and the errors you meet after login works.
Top-ups start at $5.
One shared datacenter IP for 30 days is $2.10. A single gigabyte of residential is $5.50. The balance never expires.