Selenium Proxy: Setup, Auth & Rotation (2026 Guide)
Set up a Selenium proxy in Python for Chrome and Firefox. Covers Selenium 4 Options, Selenium Wire auth, proxy rotation, and headless fixes with working code.

Setting up a Selenium proxy has one critical trap: Chrome silently ignores credentials in the --proxy-server flag. Passing --proxy-server=user:pass@host:port does nothing. Proxy authentication in Chrome needs IP whitelisting, Selenium Wire, or a packaged browser extension, and most guides skip that entirely. This guide covers every method for Selenium 4: ChromeOptions with the Service class, FirefoxOptions, the Proxy capability class, Selenium Wire for authenticated proxies, rotation patterns, headless considerations, and a complete error reference for Selenium datacenter proxy setups.
Why Use a Proxy with Selenium?
A browser automation proxy routes all browser traffic, including JavaScript-initiated requests, images, fonts, and API calls, through a datacenter IP. Unlike HTTP library proxies (requests, httpx), Selenium proxies affect the full browser network stack, making them essential for:
| Use Case | Why Proxy Is Required |
|---|---|
| Web scraping geo-restricted content | Present a specific country's datacenter IP |
| Ad verification | Verify ad rendering from the target geography |
| Price monitoring | Prevent single-IP rate limiting across hundreds of pages |
| Account management | Assign a stable IP per account to avoid association |
| QA on production sites | Test from specific regions without VPN |
When you set a proxy on a Selenium driver, every request the browser makes (page loads, XHR/fetch calls, resource downloads) goes through the proxy. This is different from configuring a proxy in Python requests, which only affects your script's HTTP calls.
If your automation runs on Puppeteer rather than Selenium, the same proxy concepts apply with different syntax, covered in how to use proxies with Puppeteer.
Selenium Proxy via ChromeOptions (IP Whitelisting)
The simplest Chrome proxy setup uses the --proxy-server argument. This works for datacenter proxies that use IP whitelisting, where your machine's IP is authorized in the proxy dashboard, so no username or password is required.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def create_driver_with_proxy(proxy_host: str, proxy_port: int) -> webdriver.Chrome:
options = Options()
options.add_argument(f"--proxy-server={proxy_host}:{proxy_port}")
options.add_argument(f"--proxy-bypass-list=localhost,127.0.0.1")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
return webdriver.Chrome(options=options)
driver = create_driver_with_proxy("your-proxy.sparkproxy.io", 10000)
driver.get("https://httpbin.org/ip")
print(driver.find_element("tag name", "pre").text)
driver.quit()
Selenium 4 note: Since Selenium 4.6, Selenium Manager downloads the matching
chromedriverautomatically, sowebdriver.Chrome(options=options)runs with no driver path. If you need a specific binary, pass aServiceobject:from selenium.webdriver.chrome.service import Service, thendriver = webdriver.Chrome(service=Service(executable_path="/path/to/chromedriver"), options=options). The proxy always goes onoptions, never onService.
The --proxy-bypass-list argument accepts a comma-separated list of hostnames and IP ranges to bypass. Use to skip the proxy for all local addresses:
options.add_argument("--proxy-bypass-list=<local>,*.internal.company.com")
SOCKS5 proxy with ChromeOptions
options = Options()
options.add_argument("--proxy-server=socks5://your-proxy.sparkproxy.io:1080")
driver = webdriver.Chrome(options=options)
Note: Chrome's
--proxy-serverflag acceptshttp://,https://, andsocks5://schemes. For most datacenter proxy setups, omit the scheme entirely, and Chrome defaults to HTTP CONNECT tunneling when no scheme is specified.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Selenium Proxy via the Proxy Capability Class
Selenium's official Proxy class provides a structured, browser-agnostic way to configure proxies, documented in the Selenium WebDriver Browser Options spec. It supports HTTP, HTTPS, SOCKS, FTP, and auto-configuration proxies.
from selenium import webdriver
from selenium.webdriver.common.proxy import Proxy, ProxyType
from selenium.webdriver.chrome.options import Options
# Build the proxy capability object
proxy = Proxy()
proxy.proxy_type = ProxyType.MANUAL
proxy.http_proxy = "your-proxy.sparkproxy.io:10000"
proxy.ssl_proxy = "your-proxy.sparkproxy.io:10000" # HTTPS
proxy.no_proxy = "localhost,127.0.0.1"
# Attach to ChromeOptions
options = Options()
options.proxy = proxy
driver = webdriver.Chrome(options=options)
driver.get("https://httpbin.org/ip")
driver.quit()
The ProxyType enum values relevant for datacenter proxies:
| ProxyType | Description |
|---|---|
| `MANUAL` | Explicit host:port for each protocol |
| `AUTODETECT` | WPAD auto-detection |
| `PAC` | Proxy Auto-Config file URL |
| `DIRECT` | No proxy (bypass all) |
| `SYSTEM` | Use OS proxy settings |
For PAC file-based proxy configuration:
proxy = Proxy()
proxy.proxy_type = ProxyType.PAC
proxy.proxy_autoconfig_url = "http://proxy.company.com/proxy.pac"
options = Options()
options.proxy = proxy
Firefox Proxy with FirefoxOptions
Firefox proxy configuration uses preference keys rather than command-line arguments. These map directly to Firefox's about:config proxy settings.
from selenium import webdriver
from selenium.webdriver.firefox.options import Options as FirefoxOptions
def create_firefox_with_proxy(host: str, port: int) -> webdriver.Firefox:
options = FirefoxOptions()
# 0 = no proxy, 1 = manual, 2 = PAC, 4 = auto-detect, 5 = system
options.set_preference("network.proxy.type", 1)
# HTTP proxy
options.set_preference("network.proxy.http", host)
options.set_preference("network.proxy.http_port", port)
# HTTPS proxy (uses same host/port for datacenter proxies)
options.set_preference("network.proxy.ssl", host)
options.set_preference("network.proxy.ssl_port", port)
# SOCKS proxy (optional, set if using SOCKS5)
# options.set_preference("network.proxy.socks", host)
# options.set_preference("network.proxy.socks_port", socks_port)
# options.set_preference("network.proxy.socks_version", 5)
# Bypass list
options.set_preference(
"network.proxy.no_proxies_on",
"localhost,127.0.0.1"
)
return webdriver.Firefox(options=options)
driver = create_firefox_with_proxy("your-proxy.sparkproxy.io", 10000)
driver.get("https://httpbin.org/ip")
driver.quit()
Firefox proxy with the Proxy class
You can also use the same Proxy capability object as Chrome:
from selenium.webdriver.common.proxy import Proxy, ProxyType
from selenium.webdriver.firefox.options import Options as FirefoxOptions
proxy = Proxy()
proxy.proxy_type = ProxyType.MANUAL
proxy.http_proxy = "your-proxy.sparkproxy.io:10000"
proxy.ssl_proxy = "your-proxy.sparkproxy.io:10000"
options = FirefoxOptions()
options.proxy = proxy
driver = webdriver.Firefox(options=options)
Proxy Authentication in Chrome: The Core Problem
This is the most misunderstood part of Selenium proxy configuration:
Chrome does not support credentials in the --proxy-server flag.
# THIS DOES NOT WORK: credentials are silently ignored
options.add_argument("--proxy-server=user:pass@your-proxy.sparkproxy.io:10000")
Chrome was intentionally designed this way. The browser expects proxy credentials to come through authentication dialogs (which WebDriver can't reliably intercept) or through dedicated credential-storage APIs. There are three practical solutions:
Solution 1: IP Whitelisting (Recommended)
Add your machine's public IP address to the proxy whitelist in your SparkProxy dashboard. No credentials needed in the driver configuration at all, since the proxy grants access based on IP. This is the cleanest approach for Selenium automation running from a fixed-IP server.
# With IP whitelisting, no credentials required
options.add_argument("--proxy-server=your-proxy.sparkproxy.io:10000")
Solution 2: Selenium Wire (see next section)
Selenium Wire intercepts the browser's proxy authentication challenge at the Python level and handles credential injection transparently. This is the recommended approach for credentials-based auth.
Solution 3: Chrome Extension for Proxy Auth
Create a packaged Chrome extension that handles the chrome.webRequest.onAuthRequired event. The extension provides credentials programmatically, bypassing the auth dialog.
import zipfile
import os
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def create_proxy_extension(host, port, username, password):
"""
Creates a Chrome extension ZIP that handles proxy authentication.
Returns the path to the generated .zip file.
"""
manifest = """
{
"version": "1.0.0",
"manifest_version": 2,
"name": "Proxy Auth",
"permissions": ["proxy", "tabs", "unlimitedStorage", "storage",
"<all_urls>", "webRequest", "webRequestBlocking"],
"background": {"scripts": ["background.js"]},
"minimum_chrome_version": "22.0.0"
}
"""
background_js = f"""
var config = {{
mode: "fixed_servers",
rules: {{
singleProxy: {{
scheme: "http",
host: "{host}",
port: parseInt("{port}")
}},
bypassList: ["localhost"]
}}
}};
chrome.proxy.settings.set({{value: config, scope: "regular"}}, function() {{}});
function callbackFn(details) {{
return {{
authCredentials: {{
username: "{username}",
password: "{password}"
}}
}};
}}
chrome.webRequest.onAuthRequired.addListener(
callbackFn,
{{urls: ["<all_urls>"]}},
["blocking"]
);
"""
ext_path = "proxy_auth_extension.zip"
with zipfile.ZipFile(ext_path, "w") as zf:
zf.writestr("manifest.json", manifest)
zf.writestr("background.js", background_js)
return ext_path
# Use the extension
ext_path = create_proxy_extension(
host="your-proxy.sparkproxy.io",
port=10000,
username="YOUR_USER",
password="YOUR_PASS",
)
options = Options()
options.add_extension(ext_path)
driver = webdriver.Chrome(options=options)
driver.get("https://httpbin.org/ip")
driver.quit()
os.remove(ext_path) # Clean up
Security note: The extension approach embeds credentials in the ZIP file in plain text. Use IP whitelisting or Selenium Wire in production, since both avoid storing credentials in files.
Selenium Wire for Authenticated Proxy (Recommended)
Selenium Wire extends Selenium's WebDriver with proxy support including authentication, request/response interception, and per-request proxy assignment. It is the cleanest solution for credential-based datacenter proxy authentication in Selenium.
pip install selenium-wire
Dependency note: selenium-wire relies on an older blinker API. On
blinker1.8 and newer you may hitAttributeError: module 'blinker' has no attribute '_saferef'at import. Pin the compatible version withpip install "blinker==1.7.0", or move to a maintained selenium-wire fork. This is a packaging issue, not a proxy problem.
from seleniumwire import webdriver # Drop-in replacement for selenium.webdriver
# Authenticated datacenter proxy
sw_options = {
"proxy": {
"http": "http://user:pass@your-proxy.sparkproxy.io:10000",
"https": "http://user:pass@your-proxy.sparkproxy.io:10000",
"no_proxy": "localhost,127.0.0.1",
}
}
driver = webdriver.Chrome(seleniumwire_options=sw_options)
driver.get("https://httpbin.org/ip")
# Bonus: Selenium Wire lets you inspect requests made by the browser
for request in driver.requests:
if request.response:
print(f"{request.url} โ {request.response.status_code}")
driver.quit()
Selenium Wire with IP whitelisting (no credentials)
from seleniumwire import webdriver
sw_options = {
"proxy": {
"http": "http://your-proxy.sparkproxy.io:10000",
"https": "http://your-proxy.sparkproxy.io:10000",
}
}
driver = webdriver.Chrome(seleniumwire_options=sw_options)
driver.get("https://httpbin.org/ip")
driver.quit()
Selenium Wire with SOCKS5
from seleniumwire import webdriver
sw_options = {
"proxy": {
"http": "socks5://user:pass@your-proxy.sparkproxy.io:1080",
"https": "socks5://user:pass@your-proxy.sparkproxy.io:1080",
}
}
driver = webdriver.Chrome(seleniumwire_options=sw_options)
Selenium Wire supports all major browsers via their respective webdriver classes: webdriver.Chrome, webdriver.Firefox, webdriver.Edge.
Proxy Rotation in Selenium
Each Selenium WebDriver session is bound to one proxy at launch, so the proxy cannot be changed mid-session via ChromeOptions. To rotate proxies, create a new driver instance for each proxy or IP.
For the full picture on rotation logic, proxy pools, and retry handling, see how to rotate proxies in Python.
Rotate by creating a new driver per session
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import random
PROXY_LIST = [
"proxy1.sparkproxy.io:10000",
"proxy2.sparkproxy.io:10001",
"proxy3.sparkproxy.io:10002",
]
def get_driver(proxy: str) -> webdriver.Chrome:
options = Options()
options.add_argument(f"--proxy-server={proxy}")
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
return webdriver.Chrome(options=options)
urls = [
"https://httpbin.org/ip",
"https://httpbin.org/user-agent",
"https://httpbin.org/headers",
]
for url in urls:
proxy = random.choice(PROXY_LIST)
driver = get_driver(proxy)
try:
driver.get(url)
print(f"[{proxy}] {driver.current_url}: OK")
finally:
driver.quit() # Always quit to free resources
Rotate with Selenium Wire per request
Selenium Wire allows changing the proxy between requests within the same session using the driver.proxy property:
from seleniumwire import webdriver
import random
PROXIES = [
{"http": "http://user:pass@proxy1.sparkproxy.io:10000",
"https": "http://user:pass@proxy1.sparkproxy.io:10000"},
{"http": "http://user:pass@proxy2.sparkproxy.io:10001",
"https": "http://user:pass@proxy2.sparkproxy.io:10001"},
]
driver = webdriver.Chrome()
for url in ["https://httpbin.org/ip", "https://httpbin.org/ip"]:
driver.proxy = random.choice(PROXIES) # Rotate before each request
driver.get(url)
print(driver.find_element("tag name", "pre").text)
driver.quit()
driver.proxy assignment in Selenium Wire takes effect for all subsequent requests in the session, with no restart required.
Headless Chrome with a Proxy
Most Selenium proxy automation runs headless on a server. The proxy configuration is identical, but two things change: how you launch the browser and how easily anti-bot systems spot the session.
Use the modern headless mode and set a real window size so pages render the way they would on a desktop:
options = Options()
options.add_argument(f"--proxy-server={proxy_host}:{proxy_port}")
options.add_argument("--headless=new") # modern headless (Chrome 109+)
options.add_argument("--window-size=1920,1080") # avoid a tiny or zero viewport
options.add_argument("--disable-gpu") # harmless on Linux servers
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage") # avoid /dev/shm crashes in Docker
driver = webdriver.Chrome(options=options)
Three things matter in production:
--headless=newvs legacy--headless. The new mode runs the same rendering path as headed Chrome, so sites see a more consistent fingerprint. The legacy flag is deprecated.- A proxy hides your IP, not your automation. Headless browsers still leak signals such as
navigator.webdriver, a headless user-agent, and a missing window size. Pair a clean IP with--window-size, a real user-agent, and--disable-blink-features=AutomationControlled. /dev/shmin Docker. Without--disable-dev-shm-usage, headless Chrome can crash on the small shared-memory segment inside containers.
A good proxy keeps your IP off the block list, but it will not rescue an obviously automated headless session. For the fingerprint and behavior side of the problem, see how to avoid getting your proxy blocked.
Selenium Grid and Remote WebDriver with Proxy
When running Selenium against a remote Grid node, pass the proxy via ChromeOptions before creating the remote session:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.proxy import Proxy, ProxyType
proxy = Proxy()
proxy.proxy_type = ProxyType.MANUAL
proxy.http_proxy = "your-proxy.sparkproxy.io:10000"
proxy.ssl_proxy = "your-proxy.sparkproxy.io:10000"
options = Options()
options.proxy = proxy
# Remote Grid session (Selenium Grid 4 / cloud provider)
driver = webdriver.Remote(
command_executor="http://selenium-hub:4444/wd/hub",
options=options,
)
driver.get("https://httpbin.org/ip")
driver.quit()
Grid note: The proxy is configured in the browser running on the Grid node, not the machine running the test. The Grid node's IP must be whitelisted in your proxy dashboard (for IP auth), or credentials must be passed via Selenium Wire or the extension method.
Test Your Proxy in Selenium
Before running your actual automation, verify the proxy is routing correctly.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import json
def verify_proxy(proxy_host: str, proxy_port: int) -> dict:
options = Options()
options.add_argument(f"--proxy-server={proxy_host}:{proxy_port}")
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=options)
try:
# Test 1: Check exit IP
driver.get("https://httpbin.org/ip")
ip_data = json.loads(driver.find_element("tag name", "pre").text)
# Test 2: Check that HTTPS also routes through proxy
driver.get("https://ipv4.icanhazip.com")
https_ip = driver.find_element("tag name", "body").text.strip()
return {
"proxy": f"{proxy_host}:{proxy_port}",
"exit_ip_httpbin": ip_data.get("origin"),
"exit_ip_https": https_ip,
"status": "ok",
}
except Exception as e:
return {"proxy": f"{proxy_host}:{proxy_port}", "status": "error", "error": str(e)}
finally:
driver.quit()
result = verify_proxy("your-proxy.sparkproxy.io", 10000)
print(result)
# {"proxy": "your-proxy.sparkproxy.io:10000", "exit_ip_httpbin": "1.2.3.4", ...}
Both exit_ip_httpbin and exit_ip_https should match the proxy's datacenter IP. If they are your real IP, the proxy is not being applied.
For a deeper checklist of proxy health checks beyond the exit IP, including DNS leaks and latency, see how to test if your proxy is working.
Common Selenium Proxy Errors and Fixes
| Error / Symptom | Cause | Fix |
|---|---|---|
| `--proxy-server=user:pass@host:port` ignored | Chrome does not support credentials in this flag | Use IP whitelisting, Selenium Wire, or the extension method |
| `ERR_TUNNEL_CONNECTION_FAILED` | Proxy host unreachable, port blocked, or proxy down | Test: `curl -x proxy-host:port https://httpbin.org/ip`; check port is open |
| `ERR_PROXY_AUTH_REQUIRED` | Credentials required but not provided | Use Selenium Wire or extension for auth; or add machine IP to whitelist |
| Browser opens, but real IP shown on target site | Proxy set but not routing correctly | Confirm `--proxy-server` argument was added before `webdriver.Chrome()` call; check no typo in host |
| `WebDriverException: unknown error: net::ERR_NO_SUPPORTED_PROXIES` | Wrong scheme used (e.g., `https://` proxy URL for Chrome) | Remove the scheme from `--proxy-server` or use `http://` |
| Selenium Wire: `OSError: [Errno 48] Address already in use` | Port conflict, Selenium Wire uses a local proxy listener | Set a custom port: `sw_options = {"port": 9999}` |
| Firefox: `SSL_ERROR_RX_RECORD_TOO_LONG` | Proxy is not configured for HTTPS (only HTTP pref set) | Set both `network.proxy.ssl` and `network.proxy.ssl_port` prefs |
| `SessionNotCreatedException: Chrome not reachable` | Extension ZIP malformed or manifest error | Validate `manifest.json` has `"manifest_version": 2` and `background.scripts` field |
| Proxy works for first request, then 407 errors | IP whitelist entry expired (DHCP IP change) | Use credential auth via Selenium Wire; or assign a static IP |
Frequently asked questions
Yes. Each webdriver.Chrome() instance is independent, so pass a different proxy in each Options object. Parallel execution with tools like pytest-xdist or concurrent.futures.ThreadPoolExecutor creates separate driver instances, each with its own proxy configuration.
Yes. Add --headless=new to ChromeOptions alongside Selenium Wire. The seleniumwire_options proxy config applies whether the browser is headless or visible. Use options.add_argument("--headless=new") (the modern headless mode) rather than the legacy --headless flag.
A requests proxy only affects your Python script's HTTP calls. A Selenium proxy affects the entire browser: every resource the page loads (HTML, CSS, JavaScript, images, XHR, WebSocket) goes through the proxy. For web scraping where JavaScript makes API calls, a Selenium proxy is the correct choice. If you also run pure HTTP scrapers, see using proxies with Python requests and aiohttp.
Use options.add_argument("--proxy-server=socks5://your-proxy:1080") for IP-whitelisted SOCKS5. For authenticated SOCKS5, use Selenium Wire: sw_options = {"proxy": {"http": "socks5://user:pass@host:1080", "https": "socks5://user:pass@host:1080"}}.
Yes. The proxy configured at session launch applies to all requests in that driver session. The only way to change proxy mid-session natively is with Selenium Wire's driver.proxy property assignment or by quitting and creating a new driver.
Yes. With Selenium Wire, inspect driver.requests after any page load. Each request shows headers including any proxy-added Via or X-Forwarded-For headers. For plain Selenium, driver.get("https://ipv4.icanhazip.com") and driver.find_element("tag name", "body").text returns just the exit IP.
No. Since Selenium 4.6, Selenium Manager resolves and downloads the matching driver, so webdriver.Chrome(options=options) runs with no driver path. Pass a Service object only when you need a specific driver binary: from selenium.webdriver.chrome.service import Service, then driver = webdriver.Chrome(service=Service(executable_path="/path/to/chromedriver"), options=options). The proxy always belongs on options, never on Service.
selenium-wire imports an old blinker API. On blinker 1.8 and newer you get AttributeError: module 'blinker' has no attribute '_saferef' at import time, before any proxy code runs. Pin the compatible release with pip install "blinker==1.7.0", or switch to a maintained selenium-wire fork. It is a dependency conflict, not a proxy misconfiguration.
Yes, because the proxy only changes your IP. Headless Chrome still exposes automation signals such as navigator.webdriver, a headless user-agent, and a zero or tiny window size, and anti-bot systems weigh those on top of the IP reputation. Run --headless=new, set --window-size, supply a real user-agent, and add --disable-blink-features=AutomationControlled. A clean proxy plus a clean fingerprint is what gets you through.
Get 50% off your first purchase
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Offer ends soon โ claim it before it's gone
Related articles

How to Scrape Airbnb Listings and Prices
Learn how to scrape Airbnb listings and prices: extract fields from Airbnb's embedded JSON, handle date-based pricing, map pagination, and anti-bot defenses.

How to Scrape GraphQL APIs
Learn to scrape GraphQL API data: find the /graphql endpoint, read the query and variables in DevTools, then replay your own queries with cursor pagination.

How to Bypass reCAPTCHA When Web Scraping
How to bypass reCAPTCHA when web scraping the ethical way: how v2 and v3 scoring work, how to raise your reCAPTCHA score, and solvers as a last resort.
