Chrome reads its proxy from a command-line switch when the process starts, so rotating a proxy in Selenium Python means one of three things: relaunch the driver with a new proxy per session, point every session at a rotating gateway that changes the exit IP for you, or intercept traffic locally with selenium-wire. Each has a different failure mode.
Why Selenium IP rotation is a process-lifecycle problem
The proxy you pass as --proxy-server=host:port is applied to the whole browser process at startup. Selenium's own Proxy capability lands in the same place: it is serialised into the session-creation payload and handed to the driver before the first navigation. Chromium documents these as network settings read at launch, and there is no supported public API to swap them for a running profile (Chromium network settings).
That leaves exactly three levers:
- New process per IP — quit the driver, launch a new one with a different proxy string.
- Stable proxy string, rotating upstream — the browser keeps one endpoint, and the proxy provider changes the exit IP per request or per session token.
- Local interception — a proxy running on
127.0.0.1(selenium-wire, mitmproxy, or your own forwarder) that re-routes upstream on demand. The browser never sees the change.
Everything you will read about Selenium proxy Python rotation is a variation on those three. Pick based on how often you need a new IP and whether you need cookies to survive rotation.
Method 1: a new driver per proxy (works with vanilla Selenium)
This is the portable option: no extra dependencies, works on Chrome, Firefox and Edge, and it also resets cookies, cache and storage — which is usually what you want when you rotate identity.
import itertools
from selenium import webdriver
from selenium.webdriver.common.by import By
PROXIES = [
"http://198.51.100.7:8000",
"http://203.0.113.24:8000",
"http://192.0.2.181:8000",
]
URLS = ["https://httpbin.org/ip"] * 3
def make_driver(proxy: str):
opts = webdriver.ChromeOptions()
opts.add_argument("--headless=new")
opts.add_argument(f"--proxy-server={proxy}")
# keep the proxy on localhost traffic out of the way
opts.add_argument("--proxy-bypass-list=<-loopback>")
return webdriver.Chrome(options=opts)
for url, proxy in zip(URLS, itertools.cycle(PROXIES)):
driver = make_driver(proxy)
try:
driver.set_page_load_timeout(30)
driver.get(url)
print(proxy, "->", driver.find_element(By.TAG_NAME, "body").text)
finally:
driver.quit()
Two costs you should measure before scaling this: browser startup is typically one to three seconds of pure overhead per rotation, and every relaunch throws away warm cache, so the same assets get downloaded again through metered bandwidth. If you rotate every request, most of your proxy spend goes on re-downloading the same CSS and fonts.
Method 2: one rotating endpoint, sticky sessions where needed
The lower-friction pattern is to give the browser a single proxy string and let the network do the rotation. That is exactly what rotating proxies are for: a fresh IP on every request from one endpoint, or a sticky session held for a defined window when you need cookies and a login to survive. Providers usually encode the choice in the username, so the Python side is just string formatting:
import uuid
from selenium import webdriver
USER, PASSWORD, HOST = "your-user", "your-pass", "gateway.example.net:8000"
def sticky_proxy(country="us", minutes=10):
sid = uuid.uuid4().hex[:8] # new token = new exit IP
# username syntax varies by provider - check their docs
user = f"{USER}-country-{country}-session-{sid}-ttl-{minutes}m"
return f"http://{user}:{PASSWORD}@{HOST}"
opts = webdriver.ChromeOptions()
opts.add_argument(f"--proxy-server={sticky_proxy()}") # creds ignored by Chrome, see below
driver = webdriver.Chrome(options=opts)
One nuance that trips people up in browser automation: a per-request rotating endpoint will send the HTML, the JS bundle and the XHR calls of a single page load out through different IPs. Plenty of sites treat that as an obvious anomaly, and any session cookie issued mid-page breaks. For Selenium, prefer a sticky session for the lifetime of a page or a task, then rotate the token. Per-request rotation belongs to HTTP clients — the pattern we cover in rotating proxies in plain Python.
Geo consistency matters too. If you pin a US exit through residential IPs, keep the browser's timezone, Accept-Language and locale aligned with it. An IP in Ohio with a Europe/Berlin timezone is a cheaper signal to detect than the IP itself.
Method 3: Selenium proxy authentication (Chrome and Firefox)
Chrome discards the user:pass@ part of --proxy-server, so authenticated proxies need one of four approaches.
IP allowlisting
If your provider supports whitelisting the machine's public IP, use it. No credentials, no extension, no interception — the most reliable option for long-running scrapers on fixed infrastructure.
A tiny extension that answers the auth prompt
Chrome exposes chrome.webRequest.onAuthRequired, so a five-line extension can supply credentials. Generate it at runtime:
import json, pathlib, tempfile
from selenium import webdriver
def auth_extension(user, password):
d = pathlib.Path(tempfile.mkdtemp())
(d / "manifest.json").write_text(json.dumps({
"manifest_version": 3,
"name": "proxy-auth", "version": "1.0",
"permissions": ["webRequest", "webRequestAuthProvider"],
"host_permissions": ["<all_urls>"],
"background": {"service_worker": "bg.js"},
}))
(d / "bg.js").write_text(
"chrome.webRequest.onAuthRequired.addListener("
f"() => ({{authCredentials: {{username: '{user}', password: '{password}'}}}}),"
"{urls: ['<all_urls>']}, ['blocking']);"
)
return str(d)
opts = webdriver.ChromeOptions()
opts.add_argument("--proxy-server=http://gateway.example.net:8000")
opts.add_argument(f"--load-extension={auth_extension('user', 'pass')}")
driver = webdriver.Chrome(options=opts)
Rebuild the extension directory whenever you rotate credentials or session tokens, and remember that extensions require the newer headless mode (--headless=new).
selenium-wire and its maintained alternatives
selenium-wire wraps the driver in a local mitm proxy, which is why it can both authenticate upstream and swap driver.proxy between navigations without relaunching the browser. It is also archived — the repository has been read-only since January 2024 (wkeeling/selenium-wire) — and it pins older blinker and pyOpenSSL versions to work on current Pythons. Use it for short-lived jobs, not for a pipeline you must maintain for a year. SeleniumBase accepts --proxy=user:pass@host:port directly and is actively maintained; most rotating proxy Python GitHub wrappers you will find are thin layers over one of these two ideas.
Firefox
For Selenium Firefox proxy authentication in Python, set the proxy through profile preferences (network.proxy.type = 1, network.proxy.http, network.proxy.ssl, network.proxy.http_port) or options.proxy. Firefox then raises a basic-auth dialog that Selenium cannot always dismiss reliably, so the practical fixes are an add-on that handles onAuthRequired, or a local credential-injecting forwarder that the browser talks to without auth. The same trick answers how to set proxy in Selenium WebDriver Java: build a org.openqa.selenium.Proxy, set setHttpProxy(), attach it to the options — and handle credentials outside the driver.
Which method to choose
| Approach | How the IP changes | Auth support | Cost per rotation | Best for |
|---|---|---|---|---|
| New driver per proxy | Process relaunch | Extension or allowlist | 1-3s + cold cache | Coarse rotation, one identity per task |
| Rotating gateway endpoint | Upstream, per request or per session token | Credentials in username (needs extension on Chrome) | Near zero | Most scraping and Selenium IP rotation at scale |
| Local interception (selenium-wire) | driver.proxy reassigned mid-session | Native | Near zero, plus mitm certificate setup | Ad-hoc jobs, debugging, request inspection |
| Scraping API | Handled by the provider | Bearer key | None | Content extraction where you do not need clicks |
Rotation cadence and retry logic that actually helps
- Rotate on outcome, not on a timer. A 403, a challenge page or a redirect to a bot-check URL is a rotation signal. A successful 200 is not.
- Pin one IP per logical session. Login, cart and pagination flows need the same exit IP; a new token per task, not per request.
- Cap retries at three per URL and change something between attempts — IP, then locale, then rendering strategy. Repeating the identical request is spend without information.
- Quarantine bad exits. If you manage a static list, track failures per proxy and drop it for an hour after two consecutive timeouts.
- Block heavy resources. Images, media and fonts are usually most of the bytes. Blocking them through CDP (
Network.setBlockedURLs) or--blink-settings=imagesEnabled=falseis the single biggest cost win in browser scraping. - Log the attempt trail. Proxy used, status, bytes, wall time. Without it you cannot tell an IP problem from a fingerprint problem — and they need opposite fixes.
The honest cost math: rotating browsers vs a scraping API
Residential bandwidth is metered, and a headless browser pulls everything a real one does. Take a JS-heavy page that transfers roughly 2.5 MB of assets. At $0.80/GB that is about $0.002 per page in bandwidth alone, before your compute — and blocked attempts consume the same bytes, so a 20% block rate pushes the effective figure closer to $0.0019. Strip images and fonts down to ~400 KB and the same page costs about $0.00023.
Now compare with fetching the same page through the Web Scraping API: $0.0002 per page for HTML/Markdown, $0.001 with JS rendering, proxies and retries included, and failures cost nothing under the pay-per-success rule.
curl https://api.quanticdata.io/v1/scrape \
-H "Authorization: Bearer $QD_API_KEY" \
-d '{ "url": "https://example.com", "render": true }'
{ "success": true,
"data": { "markdown": "# Example Domain\n..." },
"usage": { "cost_usd": 0.001 },
"retries": [ { "attempt": 1, "outcome": "ok" } ] }
The point is not that Selenium is wrong — it is that rotation is only cheap when it is coupled to a cost model. Keep the browser for flows that genuinely need clicks, form state or authenticated navigation. Move plain content extraction to a per-success API and your proxy bill stops scaling with retries.
When an agent tool replaces the rotation code entirely
If the consumer of the data is an LLM or an agent, the whole proxy layer can move behind a tool call. Our MCP server exposes search, scrape, map, crawl, batch and SEO audit as tools to Claude, Cursor, Windsurf and VS Code, with residential proxies underneath and one JSON envelope out; the same primitives are available as REST through the Web Data API for AI. Agents get structured pages without owning a proxy pool, a Chrome install or a rotation policy.
For the cases where a real browser is unavoidable — multi-step checkouts, dashboards behind a login — the trade-off is worth stating plainly: you are paying for a process, not a request. Rotate identities at task boundaries, keep the fingerprint consistent with the exit IP, and instrument every attempt. That combination fixes far more blocks than adding proxies to a list. If you are new to the wider picture, our primer on browser automation covers where Selenium fits against CDP-native drivers.