How to Use a Proxy with Selenium

Selenium

Automated traffic passed 53% of all web requests in 2025 according to Imperva's 2026 Bad Bot Report, and 27% of bot attacks now target APIs directly. Sites answered with tighter rate limits, IP reputation checks and stricter geo rules. Selenium drives a real browser, so every session leaves through the same exit IP until you change it. A proxy with Selenium puts an intermediary between the browser and the target site: the site records the proxy address instead of yours, which lets you spread load across several IPs, keep parallel sessions separate, and check how pages behave for users in other regions.

A proxy in Selenium is a proxy server configured for the browser that WebDriver launches. The browser sends its requests to the proxy, and the proxy forwards them to the target site on your behalf. Because the site sees the proxy address, you can test regional pricing, verify localised content, and keep a long test run from exhausting a per-IP request budget. Selenium applies the setting when the session starts: the proxy capability defined in the W3C WebDriver specification is read at session creation, so switching to a different address means quitting the driver and opening a new session.

 

Selenium 4.46.0 shipped on 11 July 2026 and is the version these examples target. That release fixed system proxy handling in Selenium Manager arguments and trimmed whitespace around NO_PROXY entries, two failures that used to break proxy setups without an error message. The Python bindings pull roughly 56 million downloads a month on PyPI, so almost any proxy problem you hit has been reported already. Pin the version, because Chrome moves to a two-week release cycle from Chrome 153 on 8 September 2026 and browser and driver builds will drift twice as fast.

Setting Up a Proxy in Selenium (Python Example)

Setting a proxy in Selenium takes three inputs: the host, the port, and the protocol scheme. Add them to the browser options, launch the driver, and confirm the exit address before you run anything else. Skipping that last step is the single most common reason a scraper looks configured but is running direct.

  1. Collect the host, port and protocol from your provider dashboard.
  2. Pass --proxy-server=http://host:port to ChromeOptions.
  3. Launch the driver and open a page that echoes the client IP.
  4. Compare the returned address with the proxy address.
  5. If it matches your own IP, the flag was ignored and the session went direct.

from selenium import webdriver

 

options = webdriver.ChromeOptions()

options.add_argument("--proxy-server=http://198.51.100.24:8080")

 

driver = webdriver.Chrome(options=options)

driver.get("https://httpbin.org/ip")

print(driver.page_source)

driver.quit()

Proxy with Selenium

Fig. 1. Provider dashboard and script side by side. Only three values travel between them: IP address, port and protocol scheme. The login column stays unused at this stage, because Chrome discards credentials passed in the proxy address.

In this script, Chrome is configured to use an HTTP proxy through the --proxy-server argument. When the driver opens a page that returns the client IP, the response shows the proxy address rather than the local one, which confirms that Selenium is routing traffic through the proxy. Note that the same flag also covers HTTPS requests: Chrome maps a bare proxy entry to the "other proxies" list, which handles everything not explicitly mapped to HTTP or HTTPS.

Proxy and Selenium

Fig. 2. Two possible responses from an IP echo page. The address on the left matches the dashboard entry, so the proxy is active. The address on the right is the machine's own, which means the argument was ignored and the session ran direct.

Why proxy credentials in the URL do not work in Chrome

Chrome ignores credentials written into the proxy address. The Chromium networking documentation states that Chrome does not implement cleartext username and password in manual proxy settings and will not use credentials embedded there, so --proxy-server=http://user:pass@host:port returns 407 Proxy Authentication Required or opens a login dialog. Chrome does support Basic, Digest, Negotiate and NTLM against HTTP proxies, but only through the normal credential flow. SOCKS is stricter: no authentication method is supported for SOCKSv5 in Chrome, and the request to add it was closed as Won't Fix.

Four ways to handle proxy authentication

Four options remain once the URL approach is off the table, listed here from lowest to highest maintenance cost. Pick the first one your provider and target site allow, because each step down the list adds a moving part that can fail independently of your test code and lengthens the time it takes to work out which layer actually broke.

  1. IP whitelisting. Authorise your server address on the provider side, drop the credentials from the code, and pass only host and port. Nothing to maintain and nothing to leak.
  2. WebDriver BiDi. Subscribe to network.authRequired and answer with network.continueWithAuth. Selenium 4.45 added high-level BiDi network APIs for request, response and auth handling in Python.
  3. A Manifest V3 helper extension loaded at launch, described below.
  4. A local forwarding proxy such as mitmproxy in front of the authenticated upstream, the workaround documented by the php-webdriver project.

Selenium Wire should not go into new projects. The repository was archived by its owner on 3 January 2024 and is now read only, with the final release dating to October 2022. Downstream projects have logged breakages against current Selenium, most visibly the blinker._saferef import error that surfaces as soon as a clean environment resolves newer dependencies. Plenty of guides still recommend it, including earlier versions of this one. If an existing script depends on it and works, pin every dependency and leave it alone; for anything new, use BiDi or IP whitelisting.

The credentials extension approach now requires Manifest V3. Chrome 138 was the last build that ran Manifest V2 extensions, and support was removed from the Chrome 139 branch, so proxy-auth snippets built on background pages and webRequestBlocking do nothing today. Under Manifest V3, request the webRequest and webRequestAuthProvider permissions, register onAuthRequired with asyncBlocking, and return authCredentials from the callback.

Guard the retry loop when you build that extension. Mozilla's documentation warns that a listener supplying bad credentials is called again, which turns a single typo into an endless authentication cycle that looks like a hung browser. Count attempts per request ID and fail loudly after the second one. Firefox differs here: it still supports webRequestBlocking in Manifest V3 and offers webRequestAuthProvider as well, so a cross-browser extension needs both permission sets declared.

Verifying the proxy

Verify the exit IP before every long run rather than after it fails. Load a page that returns the client address and compare the result with the proxy you configured, then check the request log in your provider dashboard as a second signal, since a cached page can lie. Two silent failure modes deserve an explicit test: requests to localhost, 127.0.0.1/8, 169.254/16 and link-local addresses skip the proxy under Chrome’s implicit bypass rules, and a proxy list ending in direct:// falls back to your real address whenever the proxy is unreachable.

Using Proxies in Other Languages (Java, JavaScript, etc.)

Selenium exposes proxy configuration in every language binding through the Proxy class. In Java, build a Proxy object, set httpProxy and sslProxy, then attach it to ChromeOptions with setProxy. One rule catches people out: the W3C specification requires socksVersion whenever socksProxy is set, and the driver must return invalid argument if it is missing. The Python Proxy class carries socksUsername and socksPassword, but there is no HTTP equivalent, which is exactly why HTTP authentication has to live outside the capability.

Proxy proxy = new Proxy();

proxy.setHttpProxy("198.51.100.24:8080");

proxy.setSslProxy("198.51.100.24:8080");

 

ChromeOptions options = new ChromeOptions();

options.setProxy(proxy);

 

WebDriver driver = new ChromeDriver(options);

Node.js follows the same pattern through the selenium-webdriver package: build Chrome options, add the --proxy-server argument with your address, and build the driver. C# uses the ChromeOptions.Proxy property, and Ruby exposes Selenium::WebDriver::Proxy with the same field names. Field names are identical across bindings because they map straight onto the wire protocol, so a configuration that works in Python translates line for line into Java or C# without guesswork.

Rotating Proxies for Web Scraping

A single proxy masks your address, but sending hundreds of requests through one IP still runs into rate limits, because the target simply counts requests per address. Rotation spreads that count across a pool. In Selenium, rotation happens at session level: the proxy capability is read at session creation, so a new address means quitting the driver and launching a new one. Budget for that, since a browser launch costs far more than a request. Rotate per batch of pages rather than per page unless the target requires otherwise.

Manual rotation means keeping a list of proxy servers and selecting a different one for each new browser session. In Python, pick a random or sequential entry from the pool for each webdriver.Chrome() call, run the task, then quit the driver. Record which address handled which task. Without that log you cannot retire the addresses that started returning errors, and a pool degrades quietly until the whole run looks broken rather than one entry in it.

import random

from selenium import webdriver

 

proxies = [

        "http://198.51.100.24:8080",

        "http://198.51.100.31:8080",

        "http://198.51.100.47:8080",

]

 

for url in target_urls:

        options = webdriver.ChromeOptions()

    options.add_argument(f"--proxy-server={random.choice(proxies)}")

        driver = webdriver.Chrome(options=options)

        try:

            driver.get(url)

            # collect data here

        finally:

            driver.quit()

Backconnect rotation moves the pool logic to the provider. You configure one gateway address in Selenium and the provider assigns a different exit IP per request or per session interval, so no rotation code lives in your project. This fits long runs where session count is high and per-session setup cost matters. The trade-off is control: you cannot hold one address across a multi-step flow, or pin a specific city, unless the gateway offers sticky sessions. Decide which of the two you need before you buy, because switching later means rewriting the session layer.

Choosing the Right Proxy Type (Residential vs Datacenter vs Mobile)

Datacenter proxies originate from cloud data centres and are not tied to a consumer ISP. They are fast and cheap per IP, and they are also the easiest category for a site to classify, because whole ranges are registered to hosting providers. For internal testing, staging environments, and targets that do not run commercial bot management, datacenter addresses are usually enough. They are the wrong choice for anything tied to a persistent account, since a range flagged once affects every address in it.

Residential proxies use addresses assigned by ISPs to home connections, so they carry the reputation of ordinary consumer traffic. That makes them the practical default for price monitoring, availability checks and regional verification, where a datacenter range would be filtered before the page renders. Proxys.io offers premium residential IPv4 in Russia and Poland from 3.6 USD per IP per month, and the individual IPv4 plan lets you choose data-center, mobile or residential IP type on the same subscription.

Mobile proxies route traffic through 3G, 4G and LTE carrier networks. Because carriers use carrier-grade NAT, a single mobile address can sit behind hundreds of real subscribers, so operators are reluctant to block them outright. That makes them the strongest option for the hardest targets and the most expensive per IP. Reserve them for cases where residential addresses have already been ruled out by testing, not as a default, since the cost difference per session is substantial and rarely justified.

Match the plan to the session profile rather than to headline price. Proxys.io individual IPv4 starts at 1.4 USD per IP per month, foreign IPv4 covers 24 countries from 1.47 USD, and shared IPv4 drops to 0.67 USD but is used by up to three people, which rules it out for anything that ties requests to an account. Dynamic proxies start at 0.27 USD and IPv6 at 0.13 USD, both useful for high-volume, low-value requests. Every plan supports HTTPS, HTTP and SOCKS.

Proxy protocols: what Chrome actually supports

Protocol choice is decided by the Chromium networking stack, not by preference. HTTPS proxies encrypt the hop to the proxy, keep target hostnames inside the CONNECT tunnel, accept client certificates and can negotiate HTTP/2. HTTP proxies defer name resolution to the proxy and support the full authentication set. SOCKSv5 resolves names proxy side but carries no authentication in Chrome and relays TCP only. SOCKSv4 resolves client side, is IPv4 only, and has no authentication at all, which makes it the weakest option on every axis.

Proxy schemes in Chrome, per the Chromium networking documentation

Scheme

Auth in Chrome

DNS resolved by

Practical limit

HTTPS

Basic, Digest, Negotiate, NTLM, client certificates

Proxy

Cannot be set through system proxy settings

HTTP

Basic, Digest, Negotiate, NTLM

Proxy

32 simultaneous connections across all domains on HTTP/1.1

SOCKSv5

None

Proxy

TCP only, no UDP relay

SOCKSv4

None

Client

IPv4 targets only, no v4a fallback

Plan parallelism around the connection cap. HTTP/1.1 proxies in Chrome are limited to 32 simultaneous connections across all domains combined, so N parallel Selenium sessions sharing one HTTP/1.1 proxy get roughly 32/N connections each. A modern page can request dozens of resources at once, so the queue builds quickly and the symptom looks like a slow proxy even when the link is idle. An HTTPS proxy negotiating HTTP/2 does not carry that restriction, which is the practical reason to prefer it for parallel runs.

Tips and Troubleshooting

Free and public proxy lists are a false economy. Addresses on them are usually slow, shared with unknown traffic, and already filtered by the sites you care about, so a run fails without telling you why. Paid addresses from a provider give you a support channel, a dashboard log, and a known IP type. Test a small paid batch against your actual target before committing to a plan size, because success rates vary by site far more than by provider marketing.

Format errors account for a large share of proxy failures. Include the scheme (http://, https:// or socks5://) in the address, and confirm the port matches the protocol your provider assigned, since HTTP and SOCKS ports usually differ. Do not put a username and password into the address. Chrome discards them, and the resulting 407 error looks like a credentials problem when it is actually a configuration one.

A 407 Proxy Authentication Required response means the proxy expected credentials it did not receive. Check the username and password first, then check whether your IP is still whitelisted, since provider dashboards drop stale entries. If the credentials are correct and still rejected, the browser is discarding them rather than the proxy refusing them, and the fix is one of the four authentication paths above rather than another attempt at the URL syntax.

WebRTC exposes the real address because Chrome’s SOCKSv5 support does not relay UDP. The documented fix is the WebRtcIPHandling policy set to disable_non_proxied_udp, which forces WebRTC to use SOCKS UDP proxying or fall back to TCP proxying; left unset, WebRTC uses every available network interface, per RFC 8828 section 5.2. Command line arguments circulating in older guides no longer exist or never worked. Test the outcome instead of trusting the flag.

Proxies add a network hop, so pages load more slowly than over a direct connection and default timeouts start firing. Raise the page load timeout to match, for example driver.set_page_load_timeout(60) in Python, and set an explicit wait for elements rather than relying on implicit ones. Running browsers through proxies is also memory-heavy, so measure concurrency per machine before scaling out rather than after a run starts failing on resources.

Chrome hides proxy failures for five minutes. When a proxy fails at connection level, Chromium marks it bad and moves it below every other entry in the list, including direct://, and holds that state for five minutes. Only two conditions count as failures for fallback: DNS resolution of the proxy, and the TCP connect. A CONNECT tunnel that returns an error does not trigger it. There is no setting to disable the bad-proxy cache, so build a health check instead of assuming a failed request means a failed proxy.

The IP is only one signal

A clean IP does not make a session look human. A February 2026 study trained gradient-boosted classifiers on 227,404 JA4 TLS fingerprints and reached 98.63% accuracy separating bad bots from benign traffic, yet the authors rate the same method Low to None against a real browser engine driven by Selenium, because the TLS stack is identical to genuine Chrome. The same paper notes that swapping the User-Agent or routing through a proxy leaves the JA4 fingerprint unchanged, and that a missing Accept-Language header is a strong scripted-client signal.

Pinning an old Chrome build undoes the work a good IP does. Selenium Manager telemetry shows 28.8% of users, over 28 million unique clients, explicitly requesting Chrome 127.0.6533.99, a release from August 2024, with the share above 40% across a rolling 28-day window. That is one of the largest identical browser-version clusters in circulation. Version is only reported when it is set deliberately through setBrowserVersion or SE_BROWSER_VERSION, so leaving it unset, as 70.1% of users do, is the safer default.

Proxies solve technical routing, not permission. Follow the terms of service of the sites you automate, respect the directives in robots.txt, keep request rates at a level the target can absorb, and handle any personal data you touch under the applicable law. Rate limiting your own crawler is worth building early: it protects the target, it keeps your addresses usable, and it is far cheaper than re-acquiring a pool after a set of IPs stops working.

Where to start

Set up in this order on a new project: whitelist your server IP with the provider so credentials never enter the code, configure a single HTTPS proxy, confirm the exit address, then add rotation only once a single address starts returning 429 responses. Keep Selenium pinned, leave the Chrome version unset, and set WebRtcIPHandling before the first production run. The W3C is reworking the proxy capability to allow different proxies per protocol and per traffic type, discussed by the Browser Testing and Tools Working Group in January 2026, so expect the configuration surface to widen in Selenium 5.