A Selenium proxy is configured through browser options: --proxy-server=http://host:port for Chrome, or network.proxy.* preferences for Firefox. Neither accepts a username and password, so the clean way to use an authenticated proxy with Selenium is to allowlist the IP your browsers connect from and pass only the host and port. When allowlisting is not possible, run a small local forwarder that adds the credentials, or, in Firefox, answer the proxy's challenge with Selenium's BiDi authentication handler.
This guide covers each route with Selenium 4 Python examples, then the parts that differ on Selenium Grid, and the timeouts, retries and exit-IP checks every proxied browser job needs. The examples read connection details from the environment: PROXY_SERVER holds a host and port with no credentials, like http://gateway.proxyforge.io:PORT, and PROXY_URL holds the full connection string, like http://USERNAME:[email protected]:PORT. Your dashboard generates both for the country and session mode you choose.
Why Chrome's --proxy-server has no place for credentials
Chrome's proxy switch takes a scheme, host and port. There is no field for a username or password, and in our tests a value containing credentials made every navigation fail with net::ERR_NO_SUPPORTED_PROXIES. When an authenticating proxy challenges Chrome with a 407, Chrome expects someone to answer: a human at a login prompt, or a tool that intercepts the challenge. A plain Selenium proxy setup with ChromeDriver has neither.
That leaves three workable designs:
| Approach | Browsers | Credentials live | Best for |
|---|---|---|---|
| IP allowlist | Chrome, Firefox, Edge | Nowhere on the client | Fixed egress: servers, CI with static NAT, Grid nodes |
| Local authenticating forwarder | Any | On the machine running the browser | Changing egress, laptops, shared runners |
| Firefox BiDi authentication handler | Firefox | In the test process | Firefox-only suites that cannot allowlist |
Playwright and Puppeteer handle the challenge in the automation library itself, which is why their proxy setup is shorter; see the Playwright proxy guide and the Puppeteer proxy guide if you have a choice of tool.
The clean path: allowlist the IP and pass only host and port
With your egress address on the allowlist, the gateway accepts connections without credentials, and the Selenium proxy configuration shrinks to the address of the gateway. For Chrome:
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument(f"--proxy-server={os.environ['PROXY_SERVER']}")
driver = webdriver.Chrome(options=options)
try:
driver.set_page_load_timeout(30)
driver.get("https://api.ipify.org?format=json")
print(driver.find_element(By.TAG_NAME, "body").text)
finally:
driver.quit()
For Firefox, set the manual proxy preferences on the options object, which Selenium writes into the temporary profile it creates:
import os
from urllib.parse import urlsplit
from selenium import webdriver
from selenium.webdriver.common.by import By
proxy = urlsplit(os.environ["PROXY_SERVER"])
options = webdriver.FirefoxOptions()
options.add_argument("-headless")
options.set_preference("network.proxy.type", 1)
options.set_preference("network.proxy.http", proxy.hostname)
options.set_preference("network.proxy.http_port", proxy.port)
options.set_preference("network.proxy.ssl", proxy.hostname)
options.set_preference("network.proxy.ssl_port", proxy.port)
options.set_preference("network.proxy.no_proxies_on", "localhost,127.0.0.1")
driver = webdriver.Firefox(options=options)
try:
driver.set_page_load_timeout(30)
driver.get("https://api.ipify.org?format=json")
print(driver.find_element(By.TAG_NAME, "body").text)
finally:
driver.quit()
network.proxy.type set to 1 means manual configuration. The ssl pair covers HTTPS pages, which is most of the web; forgetting it is the Firefox equivalent of setting only http in a Python proxies dictionary. For SOCKS5, set network.proxy.socks, network.proxy.socks_port, network.proxy.socks_version to 5 and network.proxy.socks_remote_dns to True instead, so hostnames are resolved at the exit rather than locally. SOCKS5 in either browser also depends on allowlisting, since neither will send SOCKS credentials from Selenium.
To find the address to allowlist, request an IP echo service from the machine that runs the browser, without a proxy. Behind NAT, that is the NAT gateway's public address, not the interface address. IP allowlist vs username and password covers the trade-offs of each mode in more depth, including what happens when egress addresses change.
When you cannot allowlist: a local authenticating forwarder
If the browser's egress IP changes, as it does on laptops and shared CI runners, a forwarder on the same machine can hold the credentials instead. The browser talks to 127.0.0.1 without authentication, and the forwarder relays each connection to the gateway with a Proxy-Authorization header added. This is enough for HTTP and HTTPS through CONNECT:
import asyncio
import base64
import os
from urllib.parse import unquote, urlsplit
upstream = urlsplit(os.environ["PROXY_URL"])
credentials = f"{unquote(upstream.username)}:{unquote(upstream.password)}"
auth_header = b"Proxy-Authorization: Basic " + base64.b64encode(credentials.encode())
async def pipe(reader, writer):
try:
while data := await reader.read(65536):
writer.write(data)
await writer.drain()
finally:
writer.close()
async def handle(client_reader, client_writer):
head = await client_reader.readuntil(b"\r\n\r\n")
lines = [
line for line in head.split(b"\r\n")
if line and not line.lower().startswith((b"proxy-authorization:", b"proxy-connection:", b"connection:"))
]
lines[1:1] = [auth_header, b"Connection: close"]
upstream_reader, upstream_writer = await asyncio.open_connection(upstream.hostname, upstream.port)
upstream_writer.write(b"\r\n".join(lines) + b"\r\n\r\n")
await asyncio.gather(
pipe(client_reader, upstream_writer),
pipe(upstream_reader, client_writer),
return_exceptions=True,
)
async def main():
port = int(os.environ.get("FORWARDER_PORT", "8899"))
server = await asyncio.start_server(handle, "127.0.0.1", port)
async with server:
await server.serve_forever()
asyncio.run(main())
Run it next to the browser and point Selenium at it with --proxy-server=http://127.0.0.1:8899, or the Firefox preferences above with host 127.0.0.1 and port 8899. Properties worth knowing before you rely on it:
- It does not decrypt anything. HTTPS pages pass through as an opaque
CONNECTtunnel, so there is no certificate to install and nothing changes for the target. - It binds to localhost only. Keep it that way; a forwarder listening on a public interface is an open proxy that spends your bandwidth.
- It forces
Connection: closeon each proxy connection. That keeps the credential injection correct for plain HTTP, at the cost of connection reuse forhttp://URLs. HTTPS tunnels are unaffected. - It is deliberately minimal. For a long-lived production service, an established proxy server that supports upstream authentication gives you logging, limits and health checks. The design is the same.
We tested this forwarder with Chrome and Firefox under Selenium 4, over both HTTP and HTTPS.
The Firefox alternative: BiDi authentication
Selenium 4's WebDriver BiDi support includes a network authentication handler. With options.enable_bidi = True, driver.network.add_auth_handler(username, password) answers authentication challenges from inside the test. In our tests with Selenium 4.49 and Firefox 156, it answered the gateway's 407 and the page loaded through the proxy. With Chrome 154 and ChromeDriver, the same call did not answer the proxy challenge and the navigation never completed. Treat it as a Firefox option, and re-test when you upgrade either the browser or Selenium.
Why not selenium-wire
selenium-wire was the common answer to this problem for years. Its repository is now archived and its maintainer states it is no longer maintained. It also works by running a local intercepting proxy that decrypts TLS with its own certificate authority, which changes the TLS handshake the target sees and adds a component that no longer receives security fixes. Existing suites that depend on it will keep running until something changes underneath them; new work should use one of the approaches above.
Selenium Grid and remote browsers
On Grid, the browser runs on a node, not on the machine running your test code. Every part of the Selenium proxy setup follows the browser:
- The allowlisted IP is the node's egress address. For nodes in containers or behind NAT, that is the NAT gateway's public IP. Allowlisting your laptop or CI runner does nothing for a remote browser.
- A forwarder must run where the browser can reach it, normally as a sidecar on each node. A
127.0.0.1address in the browser options refers to the node itself. - Credentials go to the node environment, not the test code. With a sidecar forwarder, the test only ever sees
http://127.0.0.1:8899, and the proxy credentials never cross the Grid protocol. - Options travel with the session request. Proxy arguments and preferences set on the options object are sent to the node as capabilities, so the same options work with
webdriver.Remote(command_executor=..., options=options).
If you use a hosted Grid, you usually cannot allowlist its shared egress addresses, and you cannot run a sidecar on its nodes. Check whether the vendor offers a tunnel or proxy setting of its own before designing around it.
Timeouts, retries and verifying the exit IP
Selenium's page load timeout defaults to 300 seconds, which is far too long for proxied traffic. Set it explicitly with driver.set_page_load_timeout(30), and use WebDriverWait for the element you need rather than waiting for full page load.
For retries, treat the WebDriver session as the unit:
- On a
TimeoutExceptionor a proxy connection error, quit the driver and start a new session, with a new sticky session from your provider if you use one. - On a target-side 403, 429 or challenge page, back off before retrying. Proxy 403 and 429 errors explains how to separate target-side blocks from proxy failures.
- Cap attempts per URL and log which proxy session served each attempt.
Browsers open many connections per page, so pair each WebDriver session with a sticky session rather than a per-request rotating endpoint; an IP that changes between the page and its API calls looks suspicious to many sites. Rotating vs sticky proxies covers how long a session should last.
Verify the exit from inside the browser, as the examples do, at the start of each session: load an IP echo page, read the address, and fail fast if it is your own. A curl check from the host proves nothing about a browser that is configured separately.
Which proxy line fits a Selenium workload
Selenium suites are often QA and monitoring rather than large-scale collection, which shapes the choice:
| Workload | Line | Notes |
|---|---|---|
| Geo-specific QA of consumer-facing pages | Residential | Sticky up to 60 minutes per WebDriver session |
| Stable test identities, allowlisted Grid nodes | ISP | Static, dedicated addresses, billed per address |
| Functional tests against your own sites | Datacenter | Dedicated addresses from our own ASN; cheapest per request |
| Mobile web and carrier-dependent flows | Mobile | Sticky up to 30 minutes; pair with a mobile viewport |
For ad and landing-page checks across countries, the ad verification use case describes a typical setup.
Running Selenium on ProxyForge
Every ProxyForge line supports IP allowlist authentication alongside username and password, which makes the clean path above available on every line: allowlist your Grid nodes' egress addresses and pass only the host and port. Keep the allowlist entries for QA and production egress addresses documented separately, so removing one environment never cuts off the other.
Pricing is pay-as-you-go from a prepaid wallet, from 1 GB or 1 IP, with a paid trial for ISP and datacenter; see the pricing page. If your suites already run against another provider, the migration process mirrors your current endpoint and authentication format so the suite can run against both during the comparison.