All guides
DEVELOPER GUIDES · 8 MIN READ

How to route Selenium browser traffic through a proxy

Set an HTTP proxy on Chrome with Selenium, verify egress and target content, and separate browser traffic from driver downloads.

How to route Selenium browser traffic through a proxy

Set Chrome’s proxy when creating the Selenium WebDriver, then verify the egress response and the target’s final origin, HTTP status, and required page marker in that browser. Selenium Manager’s own download proxy is a separate setting and does not route page requests.

Prerequisites

  • Python 3.14.6.
  • Selenium 4.49.0, installed with python -m pip install selenium==4.49.0.
  • Google Chrome 154.0.8037.58, installed from the official Chrome download page.
  • An unauthenticated HTTP proxy endpoint and permission to make bounded read-only requests to the configured egress and target URLs.
  • An egress endpoint that returns JSON with an ip field, plus a permitted target page with a stable text marker.

Warning: Browser requests can load target subresources, and a target’s GET route can still change state. Keep production disabled; start with staging or a sandbox and set a request or spend cap. This probe makes no application writes by design. Stop on an unexpected redirect or response, then close the WebDriver session.

Configure Chrome’s proxy

Save the complete example as probe.py. Bind PROXY_HOSTPORT without a scheme or credentials. Bind EGRESS_URL and TARGET_URL to approved HTTP or HTTPS endpoints, EXPECTED_EXIT to the expected exact IP address, and EXPECTED_MARKER to text in the permitted target page. Keep credentials out of both URLs.

import ipaddress
import json
import os
import time
from urllib.parse import urlsplit

from selenium import webdriver
from selenium.webdriver.chrome.options import Options


PROXY_HOSTPORT = os.environ.get("PROXY_HOSTPORT", "")
EGRESS_URL = os.environ.get("EGRESS_URL", "")
EXPECTED_EXIT = os.environ.get("EXPECTED_EXIT", "")
TARGET_URL = os.environ.get("TARGET_URL", "")
EXPECTED_MARKER = os.environ.get("EXPECTED_MARKER", "")

if not all((PROXY_HOSTPORT, EGRESS_URL, EXPECTED_EXIT, TARGET_URL, EXPECTED_MARKER)):
    raise SystemExit("Set PROXY_HOSTPORT, EGRESS_URL, EXPECTED_EXIT, TARGET_URL, and EXPECTED_MARKER")
parsed_proxy = urlsplit("http://" + PROXY_HOSTPORT)
if (
    not parsed_proxy.hostname
    or parsed_proxy.username
    or parsed_proxy.password
    or parsed_proxy.path
    or parsed_proxy.query
    or parsed_proxy.fragment
    or not parsed_proxy.port
    or not 1 <= parsed_proxy.port <= 65535
):
    raise SystemExit("PROXY_HOSTPORT must be host:port without a scheme or credentials")
try:
    for name, value in (("EGRESS_URL", EGRESS_URL), ("TARGET_URL", TARGET_URL)):
        parsed = urlsplit(value)
        if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password:
            raise SystemExit(f"{name} must be an HTTP(S) URL without credentials")
        parsed.port
    ipaddress.ip_address(EXPECTED_EXIT)
except ValueError:
    raise SystemExit("Use valid HTTP(S) URLs and a valid EXPECTED_EXIT IP address") from None


def current_document_status(driver):
    frame = driver.execute_cdp_cmd("Page.getFrameTree", {})["frameTree"]["frame"]
    deadline = time.monotonic() + 5
    while time.monotonic() < deadline:
        for entry in driver.get_log("performance"):
            event = json.loads(entry["message"])["message"]
            params = event.get("params", {})
            if (
                event.get("method") == "Network.responseReceived"
                and params.get("type") == "Document"
                and params.get("frameId") == frame["id"]
                and params.get("loaderId") == frame.get("loaderId")
            ):
                return int(params["response"]["status"])
        time.sleep(0.05)
    return None


options = Options()
options.add_argument("--headless=new")
options.add_argument(f"--proxy-server=http://{PROXY_HOSTPORT}")
options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
driver = None
try:
    driver = webdriver.Chrome(options=options)
    driver.set_page_load_timeout(15)
    driver.execute_cdp_cmd("Network.enable", {})
    driver.get(EGRESS_URL)
    egress_status = current_document_status(driver)
    try:
        observed_exit = json.loads(driver.find_element("tag name", "body").text)["ip"]
    except (KeyError, json.JSONDecodeError):
        raise RuntimeError("Egress endpoint did not return JSON with an ip field") from None
    if egress_status != 200 or observed_exit != EXPECTED_EXIT:
        raise RuntimeError("Proxy egress check did not match the expected value")

    driver.get(TARGET_URL)
    target_status = current_document_status(driver)
    target_body = driver.find_element("tag name", "body").text
    target_origin = urlsplit(TARGET_URL)
    final_origin = urlsplit(driver.current_url)
    if (final_origin.scheme, final_origin.netloc) != (target_origin.scheme, target_origin.netloc):
        raise RuntimeError("Target navigation left the configured origin")
    if target_status != 200 or EXPECTED_MARKER not in target_body:
        raise RuntimeError("Target status or required page marker did not match")
    print({"egress_match": True, "target_status": target_status, "marker_match": True})
except Exception as error:
    raise SystemExit(f"Probe failed ({type(error).__name__}); check settings and local logs") from None
finally:
    if driver is not None:
        driver.quit()

Run python probe.py. Selenium 4.49.0 uses Selenium Manager to locate a compatible driver for the installed browser. goog:loggingPrefs enables Chrome performance events; the status helper selects the final top-level document by its frame and loader identifiers. Raw events can include sensitive request details, so keep them private and do not print or attach them to a public log.

A successful fixture run prints:

{'egress_match': True, 'target_status': 200, 'marker_match': True}

egress_match means the egress JSON IP exactly matched EXPECTED_EXIT; target_status is the top-level target response status; and marker_match means the body contained EXPECTED_MARKER. The fixture returned a reserved documentation IP, not a real proxy exit.

The Selenium options guide shows browser options and the Python Proxy API defines proxy capability fields. This narrow example sets Chrome’s HTTP proxy with a launch argument and supports an unauthenticated endpoint. It checks an exact JSON ip value, a final target origin, HTTP 200, and required body text before it reports success.

Separate browser requests from driver downloads

The browser uses --proxy-server for page traffic. Selenium Manager uses its own network connection to find or download browsers and drivers. Its SE_PROXY setting configures that separate management request; it does not replace the Chrome option above. The Selenium Manager guide documents both paths.

Chrome does not use credentials embedded in manual proxy settings, according to the Chrome proxy guide. Do not place user:password@host:port in --proxy-server, command arguments, or source. For authenticated routes, select an extension, enterprise policy, or provider integration that documents browser support and secret handling. Do not copy proxy credentials into the target’s Authorization header. Chrome’s SOCKS5 support also does not provide proxy authentication.

An HTTP proxy connection is not encrypted between Chrome and the proxy. HTTPS target traffic uses a CONNECT tunnel and keeps TLS between Chrome and the target, but the proxy sees the target hostname. An HTTPS proxy encrypts the browser-to-proxy hop. Confirm the endpoint scheme and provider auth method before sending secrets.

Diagnose the connection

Evidence Likely layer Next check
Chrome or driver fails before navigation Browser setup Check Chrome, Selenium, Selenium Manager, and driver startup output
Navigation fails before response headers Browser-to-proxy route Check protocol, host, port, firewall, and runner reachability
Egress JSON or IP differs Proxy route or egress endpoint Check the expected exit and exact JSON ip field
Target returns a non-200 status Target response Inspect the authorized staging response and redirect behavior
Target returns 200 without the marker Page content Check the required text, access policy, and selector or render timing
Browser does not use the expected proxy Proxy configuration Inspect the browser capability and any bypass rules

The page-load timeout bounds a navigation wait; it does not bound the entire job or all network activity. This example uses 2 navigations and no application write, but Chrome can request page resources as well. Set explicit request or spend limits in the runner. For a command-line proxy check, continue with the curl proxy guide.

The local proof used Selenium 4.49.0, Python 3.14.6, Chrome 154.0.8037.58, and an unauthenticated loopback-only forwarding fixture. The fixture returned the reserved test IP 198.51.100.7; it was not a public exit measurement. The run confirms browser routing and response checks against that fixture, not provider egress or target access in production.

Sources and further reading

ProxyLaneTraffic never expires

Premium residential proxies
From $2/GB

Scale your workflows with the tools you already use. Rotating or sticky sessions up to 72 hours.

Try 350 MB for $1.95 View plans

Keep reading

Amazon Scraper API vs Proxy: Choose a Product Data Source

Compare seller APIs, licensed Amazon product data and proxy page checks by record, access scope and reuse rights.

Read guide

Antidetect browser proxy setup: choose a profile guide

Choose the right anti-detect browser proxy guide, map the shared verification steps, and keep account access separate from the network route.

Read guide

Best Proxies for Market Research: Choose by Sample and Cost

Our picks for market research: ProxyLane for small, intermittent projects, plus options for regional tracking, monthly collection and managed extraction.

Read guide