Skip to content
Article / 8 min read

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

  • requests and httpx take 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:

  1. Always quit in a finally block. Orphaned chromedriver processes hold memory and file locks.
  2. 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.
  3. Use --no-sandbox only 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 / finally everywhere.
  • 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-server flag 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.