Selenium Rotating Proxies in Python: ChromeOptions, Auth, and Per-Session Rotation
Selenium binds a proxy to the browser launch, not to the request — which makes rotation awkward and authenticated rotation awkward in a different way. Here are the three approaches that actually work in Selenium 4, with Python code for ChromeOptions, an a
Selenium has no concept of a rotating proxy. The browser session and the exit IP are bound together at launch: you pass --proxy-server= into ChromeOptions, the driver starts, and every request that browser makes goes through that endpoint until you quit it.
That single design fact explains why searches for selenium rotating proxy return so much contradictory advice. Some guides relaunch the browser once per proxy. Others reach for a wrapper library. Both work — but they fail in different places, and the thing that breaks most first attempts is authentication, not rotation itself.
This guide covers the mechanics as they apply to Chrome and Chromium driven by Selenium 4 in Python: what the proxy flags actually accept, three workable ways to rotate, how to verify the exit IP, and the pitfalls that cost the most time.
Why Selenium rotation differs from requests, httpx, or Scrapy
requestsandhttpxtake the proxy as a per-call argument. Rotation is a dictionary change.- Scrapy can swap proxies between requests in a downloader middleware, with no session restart.
- Selenium sets the proxy on the browser process. There is no supported per-request proxy argument, so mid-session rotation requires an interception layer such as selenium-wire, or DevTools Protocol work.
The second constraint is Chrome's own flag. --proxy-server=socks5://host:port accepts a scheme, a host and a port — and nothing else. There is no credentials field. If the proxy demands authentication, Chrome surfaces an HTTP auth dialog that Selenium cannot fill and that headless mode never renders at all. Some SOCKS5 endpoints similarly refuse the connection without credentials the flag has nowhere to carry.
So there are really two problems to solve separately: rotate the exit IP, and get credentials to the proxy.
Three approaches, compared
| Approach | Rotation granularity | Handles credentials | Where it hurts |
|---|---|---|---|
| Relaunch the driver per proxy | Per browser session | No — pair with IP allowlisting | Every rotation costs a browser start |
Auth extension via --load-extension |
Per browser session | Yes | Extension plumbing and headless quirks |
| selenium-wire | Per request | Yes | Extra dependency plus a local interception layer |
Most browser automation does not need per-request rotation. If your workflow is open a page, wait for JavaScript, extract, move on, per-session rotation is usually enough and far easier to debug.
Approach 1: relaunch the driver per proxy
This is the least magical option and the easiest to reason about in production.
import random
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
# Documentation ranges from RFC 5737 - replace with your own endpoints.
PROXIES = [
'http://198.51.100.10:8080',
'http://198.51.100.11:8080',
'socks5://203.0.113.7:1080',
]
def build_driver(proxy_url: str) -> webdriver.Chrome:
options = Options()
options.add_argument(f'--proxy-server={proxy_url}')
options.add_argument('--headless=new')
options.add_argument('--disable-dev-shm-usage')
return webdriver.Chrome(options=options)
def exit_ip(driver: webdriver.Chrome) -> str:
driver.get('https://api.ipify.org')
return driver.find_element(By.TAG_NAME, 'body').text.strip()
for proxy in random.sample(PROXIES, len(PROXIES)):
driver = build_driver(proxy)
try:
print(f'{proxy} -> {exit_ip(driver)}')
finally:
driver.quit()
Three habits separate a script that works from one that mysteriously dies overnight:
- Always quit in a
finallyblock. Orphaned chromedriver processes hold memory and file locks. - Give each parallel worker its own profile directory with
--user-data-dir=/tmp/chrome-worker-1. Two Chrome instances sharing a profile will contend for the lock. - Use
--no-sandboxonly in containers you control, never on a normal desktop session.
This approach works cleanly with unauthenticated proxies, which is where IP allowlisting comes in.
Approach 2: getting credentials to the browser
Option A: allowlist your own egress IP
Many providers let you authorise the IP you call from, at which point the proxy accepts you without a username or password and Approach 1 works unchanged. Two caveats: your egress IP must be stable, which is awkward in CI or on a home connection that reconnects, and an allowlisted IP is a standing authorisation you need to remember to revoke.
Option B: an extension that answers the auth prompt
Chrome extensions can supply proxy credentials through chrome.webRequest.onAuthRequired. A minimal Manifest V3 pair looks like this.
{
"manifest_version": 3,
"name": "proxy-auth",
"version": "1.0",
"permissions": ["webRequest", "webRequestAuthProvider"],
"host_permissions": ["<all_urls>"],
"background": { "service_worker": "background.js" }
}
chrome.webRequest.onAuthRequired.addListener(
(details, callback) => {
callback({ authCredentials: { username: 'USER', password: 'PASS' } });
},
{ urls: ['<all_urls>'] },
['asyncBlocking']
);
Load it when the driver starts:
options.add_argument('--disable-extensions-except=/opt/proxy-auth')
options.add_argument('--load-extension=/opt/proxy-auth')
Two things to know before you build on this. Auth callback handling has changed across Chrome releases; if the callback form is refused, return the credentials object from the listener instead of calling callback(). And extension loading in headless mode is the part most likely to surprise you — use --headless=new rather than the legacy headless mode.
Option C: selenium-wire
If you genuinely need per-request or mid-session rotation, selenium-wire puts a small local proxy in front of the browser and lets you set the upstream proxy in Python.
from selenium.webdriver.common.by import By
from seleniumwire import webdriver
proxy = 'http://USER:[email protected]:8080'
wire_options = {
'proxy': {
'http': proxy,
'https': proxy,
'no_proxy': 'localhost,127.0.0.1',
}
}
driver = webdriver.Chrome(seleniumwire_options=wire_options)
try:
driver.get('https://api.ipify.org')
print(driver.find_element(By.TAG_NAME, 'body').text)
finally:
driver.quit()
Because a local interception layer is involved, pin selenium-wire and Selenium versions together and re-test after either one is upgraded.
Do you need SOCKS5 for Selenium specifically?
Usually no, but it is supported. Chrome accepts socks5://host:port in --proxy-server, and SOCKS5 hands off traffic at the socket layer, which is useful when your pool is SOCKS-only. The credentials limitation is identical: the flag has nowhere to put a username, so you are back to allowlisting or an extension.
One clarification that matters for verification: routing the browser through SOCKS5 controls TCP traffic. It does not, by itself, stop WebRTC from opening a non-proxied UDP path. See the checklist below.
Choosing a rotation strategy for browser automation
| Strategy | Rotation trigger | Fits | Trade-offs |
|---|---|---|---|
| Sticky session per task | At task start | Logins, multi-step flows, paginated lists | One IP absorbs the whole task |
| Per N pages or per time window | Counter or timer | Broad crawling, listing pages | Rotating mid-flow can break state |
| Per browser session | Every driver launch | Straightforward scraping and screenshot jobs | Launch cost dominates runtime |
| One proxy per worker | At worker start | Parallel crawls | Per-IP load needs tracking |
Because a browser session is expensive, per-request rotation is usually the wrong default for Selenium — you pay a process restart for every URL. Location consistency also matters more than raw IP volume for geo-dependent results. If you are sampling localised pages, storefronts or prices for a particular market, hold the exit region fixed for the whole session, or the numbers you collect will not be reproducible on the next run.
Verifying the exit IP, DNS and WebRTC
Verification is not optional for headless runs, because there is no window to glance at. Check three things.
1. The exit IP actually changed. Reuse the exit_ip helper above and assert the value differs from your own address. Compare against a second echo endpoint occasionally so you are not fooled by a cached response.
2. DNS resolution behaviour. With an HTTP proxy the hostname is normally resolved by the proxy, which is what you want. SOCKS5 handling is worth confirming rather than assuming: open a public DNS leak test page in a non-headless run and compare the resolver shown with your ISP's.
3. WebRTC. Chrome may use a non-proxied UDP path even while the proxy is configured. The flag below forces WebRTC to avoid non-proxied UDP.
options.add_argument('--force-webrtc-ip-handling-policy=disable_non_proxied_udp')
Finally, log the proxy used and the observed exit IP for every session. When a page returns a challenge, the wrong language, or a price you did not expect, that log line is the first thing you will want.
Pitfalls that waste the most time
- Driver leakage. Every orphaned chromedriver holds memory. In a container this usually looks like a mysterious crash rather than a leak. Use
try/finallyeverywhere. - Shared profile directories. Parallel instances sharing a user data dir block each other. One directory per worker.
- Reading failures as blocks. A 403, an empty response and a challenge page are three different problems. Log status code, page title and body length before blaming the proxy.
- Extrapolating from one page. Verify the exit IP before automating a hundred pages, not after.
- Forgetting the non-technical rules. Rotating an IP does not change what a site's terms of service, its robots.txt, or applicable data-protection law permit — particularly if you are collecting personal data in a GDPR jurisdiction. Slow down, back off on errors, and keep concurrency modest.
The short version
- Selenium binds a proxy to a browser launch, so rotation means restarts unless you add an interception layer.
- Chrome's
--proxy-serverflag cannot carry credentials. Choose IP allowlisting, an auth extension, or selenium-wire. - Prefer per-session or sticky rotation over per-request rotation for browser automation.
- Verify the exit IP inside the session, disable non-proxied WebRTC UDP, and log which proxy served which request.
- Quit the driver in
finally, isolate profile directories per worker, and prove the setup on one URL before scaling it.