Why Selenium Proxy Auth Is Still Painful
If you've ever passed --proxy-server=http://user:pass@host:port to ChromeDriver and watched the browser silently ignore the credentials, you already know the core problem: standard Selenium has no native mechanism for proxy authentication. The WebDriver protocol simply doesn't expose a username/password field for proxy configuration. Chrome and Firefox both pop a native authentication dialog that Selenium can't dismiss programmatically.
This matters more than ever. Residential and mobile proxies — the kind that actually survive modern anti-bot systems — almost always require authentication. If you're building a Selenium pipeline on top of residential proxies, you need a real solution, not a workaround that breaks on the next Chrome update.
In this guide we'll go framework-idiomatic: proper middleware, proper extension points, proper scaling patterns. No hacks.
Chrome + selenium-wire: Authenticated Proxies Without Extensions
selenium-wire extends Selenium's ChromeDriver by intercepting requests at the network layer. It wraps the WebDriver instance so you can set authenticated proxy credentials directly — no Chrome extension, no popup dialog, no fragile Alert handling.
How It Works Under the Hood
selenium-wire runs a lightweight local mitmproxy instance. Chrome connects to localhost:PORT (unauthenticated), and selenium-wire forwards traffic to your upstream proxy with the credentials you provided. This means the browser never sees an auth challenge, and you get full request/response inspection for free.
Code: selenium-wire With ProxyHat Residential Proxies
from seleniumwire import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
PROXY_USER = "user-country-US"
PROXY_PASS = "your_password"
PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080
chrome_options = Options()
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
# selenium-wire proxy config — this is where auth lives
seleniumwire_options = {
"proxy": {
"http": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
"https": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{{PROXY_PORT}}",
"no_proxy": "localhost,127.0.0.1",
},
"disable_capture": True, # skip storing bodies to save memory
}
service = Service("/path/to/chromedriver")
driver = webdriver.Chrome(
options=chrome_options,
seleniumwire_options=seleniumwire_options,
service=service,
)
driver.get("https://httpbin.org/ip")
print(driver.find_element("tag name", "body").text)
driver.quit()
Tip: Set
disable_capture: Truein production. selenium-wire stores every request body in memory by default, which will OOM long-running fleets.
Firefox: Proxy Profile Generation
Firefox is the alternative when Chrome's fingerprint surface is too well-known. The idiomatic approach is to generate a dedicated profile with proxy settings baked in, then load it via webdriver.FirefoxProfile().
Code: Firefox Profile With Authenticated Proxy
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
PROXY_USER = "user-country-DE-city-berlin"
PROXY_PASS = "your_password"
PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080
profile = webdriver.FirefoxProfile()
# Network proxy type = 1 (manual proxy configuration)
profile.set_preference("network.proxy.type", 1)
profile.set_preference("network.proxy.http", PROXY_HOST)
profile.set_preference("network.proxy.http_port", PROXY_PORT)
profile.set_preference("network.proxy.ssl", PROXY_HOST)
profile.set_preference("network.proxy.ssl_port", PROXY_PORT)
# Do not proxy localhost
profile.set_preference("network.proxy.no_proxies_on", "localhost,127.0.0.1")
# Auth credentials — Firefox supports this via preferences
profile.set_preference("network.proxy.http.user", PROXY_USER)
profile.set_preference("network.proxy.http.pass", PROXY_PASS)
profile.set_preference("network.proxy.ssl.user", PROXY_USER)
profile.set_preference("network.proxy.ssl.pass", PROXY_PASS)
options = Options()
options.add_argument("--headless")
options.profile = profile
service = Service("/path/to/geckodriver")
driver = webdriver.Firefox(service=service, options=options)
driver.get("https://httpbin.org/ip")
print(driver.find_element("tag name", "body").text)
driver.quit()
Firefox's proxy auth via preferences is less officially documented than Chrome's extension approach, but it's been stable across ESR releases. If you hit issues on specific versions, fall back to a local proxy forwarder like tinyproxy or 3proxy that injects credentials upstream.
Selenium Stealth: Reducing Automation Fingerprints
Even with the right proxy, a vanilla Selenium session is trivially detectable. The navigator.webdriver flag, missing Notification permissions, Chrome DevTools protocol leaks, and canvas fingerprint inconsistencies all give you away. Two libraries address this:
- undetected-chromedriver — patches Chromedriver on the fly to remove
$cdc_signatures and setsnavigator.webdriver = false. - selenium-stealth — applies a suite of JavaScript patches (navigator properties, permissions API, WebGL vendor, etc.) after session creation.
Code: selenium-stealth + selenium-wire
from seleniumwire import webdriver
from selenium_stealth import stealth
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
PROXY_USER = "user-country-US-session-abc123"
PROXY_PASS = "your_password"
chrome_options = Options()
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--disable-blink-features=AutomationControlled")
seleniumwire_options = {
"proxy": {
"http": f"http://{PROXY_USER}:{PROXY_PASS}@gate.proxyhat.com:8080",
"https": f"http://{PROXY_USER}:{PROXY_PASS}@gate.proxyhat.com:8080",
},
"disable_capture": True,
}
service = Service("/path/to/chromedriver")
driver = webdriver.Chrome(
options=chrome_options,
seleniumwire_options=seleniumwire_options,
service=service,
)
# Apply stealth patches after session is created
stealth(
driver,
languages=["en-US", "en"],
vendor="Google Inc.",
platform="Win32",
webgl_vendor="Intel Inc.",
renderer="Intel Iris OpenGL Engine",
fix_hairline=True,
)
driver.get("https://bot.sannysoft.com/")
print("Stealth test page loaded. Check results.")
# driver.save_screenshot("stealth_check.png")
driver.quit()
Key insight: Always call
stealth()after the driver session is created but before any navigation. The patches inject JavaScript that must run before the page's own scripts execute.
When to Consider selenium-driverless
selenium-driverless takes a different approach: it communicates with Chrome via the DevTools Protocol directly, without a WebDriver intermediary. This eliminates the $cdc_ variable in Chromedriver entirely. It's newer and less battle-tested, but for high-stakes targets it's worth evaluating. The API mirrors Selenium's, so migration is straightforward.
Rotating Proxy Pool Pattern
Static IPs get blocked. The production pattern is a proxy pool where each new WebDriver session (or each request within a session) gets a fresh IP. ProxyHat supports this natively via the -session- flag in the username — each unique session string maps to a different residential exit node.
Code: Session-Level IP Rotation Middleware
import uuid
import time
from contextlib import contextmanager
from seleniumwire import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium_stealth import stealth
PROXY_USER_BASE = "user-country-US"
PROXY_PASS = "your_password"
PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080
def _build_proxy_url(session_id: str) -> str:
"""Inject a unique session flag so ProxyHat assigns a fresh IP."""
username = f"{PROXY_USER_BASE}-session-{session_id}"
return f"http://{username}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}"
@contextmanager
def rotating_driver(headless: bool = True, country: str = "US"):
"""Yield a fresh WebDriver with a unique residential IP."""
session_id = uuid.uuid4().hex[:12]
proxy_url = _build_proxy_url(session_id)
chrome_options = Options()
if headless:
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--disable-blink-features=AutomationControlled")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
seleniumwire_options = {
"proxy": {
"http": proxy_url,
"https": proxy_url,
},
"disable_capture": True,
}
service = Service("/path/to/chromedriver")
driver = webdriver.Chrome(
options=chrome_options,
seleniumwire_options=seleniumwire_options,
service=service,
)
stealth(driver,
languages=["en-US", "en"],
vendor="Google Inc.",
platform="Win32",
webgl_vendor="Intel Inc.",
renderer="Intel Iris OpenGL Engine",
fix_hairline=True)
try:
yield driver
finally:
driver.quit()
# --- Usage: each context gets a different IP ---
for i in range(5):
with rotating_driver() as driver:
driver.get("https://httpbin.org/ip")
ip = driver.find_element("tag name", "body").text.strip()
print(f"Session {i}: {ip}")
time.sleep(1) # polite delay between sessions
For per-request rotation within a single session, you can swap the proxy dynamically using selenium-wire's driver.proxy property — but be aware that changing the upstream proxy mid-session may leak your real IP during the switch. Session-level rotation is safer and more predictable.
Selenium Grid + Docker: Parallel Scraping at Scale
Running one browser at a time won't get you through a million pages before the data goes stale. Selenium Grid lets you distribute WebDriver sessions across a fleet of nodes, each with its own proxy assignment.
Code: Docker Compose for a Scaled Grid
# docker-compose.yml
version: "3.8"
services:
selenium-hub:
image: selenium/hub:4.18
ports:
- "4442:4442"
- "4443:4443"
- "4444:4444"
environment:
- SE_SESSION_REQUEST_TIMEOUT=300
- SE_NODE_SESSION_TIMEOUT=300
chrome-node:
image: selenium/node-chrome:4.18
depends_on:
- selenium-hub
deploy:
replicas: 8
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_VNC_NO_PASSWORD=1
shm_size: "2gb"
# Your scraper workers
scraper:
build: ./scraper
depends_on:
- selenium-hub
deploy:
replicas: 4
environment:
- HUB_URL=http://selenium-hub:4444/wd/hub
- PROXY_HOST=gate.proxyhat.com
- PROXY_PORT=8080
- PROXY_USER=user-country-US
- PROXY_PASS=${PROXYHAT_PASS}
Code: Remote WebDriver With Per-Node Proxy Assignment
import os
import uuid
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium_stealth import stealth
HUB_URL = os.environ["HUB_URL"]
PROXY_USER_BASE = os.environ["PROXY_USER"]
PROXY_PASS = os.environ["PROXY_PASS"]
PROXY_HOST = os.environ["PROXY_HOST"]
PROXY_PORT = os.environ["PROXY_PORT"]
def create_remote_driver(country: str = "US") -> webdriver.Remote:
session_id = uuid.uuid4().hex[:12]
proxy_url = (
f"http://{PROXY_USER_BASE}-country-{country}-session-{session_id}"
f":{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}"
)
# For Remote WebDriver, use Chrome extension approach
# since selenium-wire doesn't support Remote sessions.
# Build a minimal proxy auth extension on the fly.
import zipfile, base64, tempfile
manifest_json = '{"version":"1.0.0","manifest_version":2,"name":"proxy-auth","permissions":["proxy","webRequest","webRequestBlocking","<all_urls>"],"background":{"scripts":["background.js"]}}'
background_js = f"""
var config = {{
mode: "fixed_servers",
rules: {{
singleProxy: {{scheme: "http", host: "{PROXY_HOST}", port: parseInt("{PROXY_PORT}")}},
bypassList: ["localhost"]
}}
}};
chrome.proxy.settings.set({{value: config, scope: "regular"}}, function() {{}});
chrome.webRequest.onAuthRequired.addListener(
function(details) {{
return {{authCredentials: {{username: "{PROXY_USER_BASE}-country-{country}-session-{session_id}", password: "{PROXY_PASS}"}}}};
}},
{{urls: ["<all_urls>"]}},
["blocking"]
);
"""
plugin_file = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
with zipfile.ZipFile(plugin_file.name, "w") as zf:
zf.writestr("manifest.json", manifest_json)
zf.writestr("background.js", background_js)
chrome_options = Options()
chrome_options.add_argument(f"--load-extension={plugin_file.name}")
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--disable-blink-features=AutomationControlled")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
caps = chrome_options.to_capabilities()
driver = webdriver.Remote(command_executor=HUB_URL, desired_capabilities=caps)
stealth(driver,
languages=["en-US", "en"],
vendor="Google Inc.",
platform="Win32",
webgl_vendor="Intel Inc.",
renderer="Intel Iris OpenGL Engine",
fix_hairline=True)
return driver
# Run across multiple nodes in parallel
from concurrent.futures import ThreadPoolExecutor, as_completed
targets = [
"https://httpbin.org/ip",
"https://bot.sannysoft.com/",
"https://browserleaks.com/ip",
]
with ThreadPoolExecutor(max_workers=4) as pool:
futures = {pool.submit(create_remote_driver): url for url in targets}
for future in as_completed(futures):
driver = future.result()
try:
url = futures[future]
driver.get(url)
print(f"{url} → {driver.title}")
finally:
driver.quit()
Scaling note: Each Chrome node consumes ~300–500 MB of RAM. An 8-node replica fleet on a 16 GB host leaves headroom for the hub and scraper workers. Increase
replicashorizontally across multiple machines for production workloads.
Selenium vs Playwright: When to Switch
Playwright has become the default recommendation for new scraping projects. But Selenium isn't dead — and for teams with legacy test suites, migration isn't always practical. Here's an honest comparison:
| Feature | Selenium | Playwright |
|---|---|---|
| Authenticated proxy support | Requires selenium-wire or extension hack | Native — proxy: {username, password} |
| Stealth / anti-detection | Third-party (selenium-stealth, undetected-chromedriver) | Built-in (fewer leaks, but not invisible) |
| Browser support | Chrome, Firefox, Edge, Safari, IE | Chromium, Firefox, WebKit |
| Parallel execution | Selenium Grid (complex setup) | Native browser contexts, no Grid needed |
| Ecosystem maturity | 20+ years, massive plugin ecosystem | ~5 years, growing fast |
| Legacy test compatibility | Excellent — most QA suites already use it | Requires full rewrite |
| Mobile device emulation | Limited, manual | Built-in device descriptors |
| Request interception | selenium-wire adds this | Native via page.route() |
When Playwright Is the Better Choice
- New projects where no Selenium legacy exists — Playwright's API is cleaner and proxy auth is first-class.
- Heavy request interception — Playwright's
page.route()is far more ergonomic than selenium-wire's approach. - Multi-browser testing with WebKit — Selenium can't drive WebKit directly.
- High-concurrency scraping — Playwright's browser contexts are lightweight; you don't need a Grid to parallelize.
When to Stick With Selenium
- Existing QA suites — thousands of Selenium tests that work; migration cost is real.
- Safari and IE — Playwright doesn't support either.
- Cloud provider integrations — BrowserStack, Sauce Labs, LambdaTest all have first-class Selenium support; Playwright support is newer and sometimes limited.
- Team expertise — your team knows Selenium's edge cases; that knowledge has value.
The pragmatic path: keep Selenium for existing suites, use Playwright for new scraping projects. And regardless of framework, pair your browser with residential proxies for reliability.
Best Practices for Production Selenium + Proxy Pipelines
- Always use residential or mobile proxies for scraping — datacenter IPs are blocklisted by most anti-bot vendors within hours.
- Rotate at the session level, not the request level, to avoid IP leaks during proxy switches.
- Apply stealth patches before navigation — order matters.
- Set
disable_capture: Truein selenium-wire to prevent memory bloat on long runs. - Use sticky sessions (ProxyHat's
-session-flag) when a target requires login state to persist across multiple page loads on the same IP. - Respect robots.txt and rate limits — just because you can scrape doesn't mean you should hammer a server. Add delays, honor
Crawl-Delay, and consider GDPR/CCPA obligations for personal data. - Monitor success rates — track HTTP status codes and CAPTCHA triggers per proxy session. When a session's success rate drops below ~80%, kill it and start a new one.
- Containerize everything — Docker + Selenium Grid gives you reproducible environments and horizontal scaling without SSHing into machines.
Key Takeaways
- Standard Selenium cannot handle proxy authentication — use selenium-wire (Chrome) or Firefox profile preferences to inject credentials.
- selenium-stealth and undetected-chromedriver patch the most common automation fingerprints, but they're not silver bullets — layer them with residential proxies for best results.
- Rotate IPs at the session level using ProxyHat's
-session-{id}flag — each WebDriver instance gets a unique residential exit node. - Selenium Grid + Docker is the scaling pattern for parallel scraping; use Chrome proxy auth extensions when selenium-wire can't attach to Remote sessions.
- Playwright is superior for new projects (native proxy auth, browser contexts, request interception), but Selenium remains the right choice for legacy suites, Safari/IE, and cloud-provider integrations.
Ready to build? Grab your credentials from the ProxyHat dashboard, pick a country, and start rotating. The code above will get you from zero to a production-grade proxy-aware Selenium fleet in an afternoon.






