Playwright and Selenium
Configuring proxy authentication in browser automation frameworks, plus per-context proxies and rotation.
Browser automation shares one recurring headache: Chromium's --proxy-server flag does not accept credentials. Writing --proxy-server=http://user:pass@host:port silently drops them, and the browser hangs on an authentication dialog. Each framework has its own workaround.
Playwright (Python)
Playwright takes credentials separately, which is the cleanest option available:
from playwright.sync_api import sync_playwright
PROXY = {
"server": "http://GATEWAY_HOST:58971",
"username": "USERNAME-country-US-session-job1-time-30",
"password": "PASSWORD",
}
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
context = browser.new_context(proxy=PROXY)
page = context.new_page()
page.goto("https://api.ipify.org?format=json")
print(page.inner_text("body"))
context.close()
browser.close()
One session per context
from playwright.sync_api import sync_playwright
GATEWAY = "http://GATEWAY_HOST:58971"
BASE_USER = "USERNAME"
PASSWORD = "PASSWORD"
def proxy_for(session_id: str, country: str = "US", minutes: int = 30) -> dict:
return {
"server": GATEWAY,
# session values allow only [A-Za-z0-9.-]; no underscores
"username": f"{BASE_USER}-country-{country}-session-{session_id}-time-{minutes}",
"password": PASSWORD,
}
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
for index in range(4):
context = browser.new_context(
proxy=proxy_for(f"ctx.{index}"),
locale="en-US",
timezone_id="America/Los_Angeles",
viewport={"width": 1440, "height": 900},
)
page = context.new_page()
page.goto("https://api.ipify.org", wait_until="domcontentloaded")
print(index, page.inner_text("body"))
context.close()
browser.close()
Async version
import asyncio
from playwright.async_api import async_playwright
async def scrape(session_id: str, url: str) -> str:
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=True)
context = await browser.new_context(proxy={
"server": "http://GATEWAY_HOST:58971",
"username": f"USERNAME-country-US-session-{session_id}-time-30",
"password": "PASSWORD",
})
page = await context.new_page()
await page.goto(url, wait_until="domcontentloaded")
text = await page.inner_text("body")
await browser.close()
return text
async def main() -> None:
results = await asyncio.gather(*(
scrape(f"job.{i}", "https://api.ipify.org") for i in range(4)
))
for result in results:
print(result)
asyncio.run(main())
Playwright (Node.js)
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
proxy: {
server: 'http://GATEWAY_HOST:58971',
username: 'USERNAME-country-US-session-job1-time-30',
password: 'PASSWORD',
},
locale: 'en-US',
timezoneId: 'America/Los_Angeles',
});
const page = await context.newPage();
await page.goto('https://api.ipify.org?format=json');
console.log(await page.innerText('body'));
await browser.close();
Puppeteer
Puppeteer has no proxy object like Playwright's; credentials go through page.authenticate():
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: true,
// Launch flags carry host:port only; credentials cannot go here
args: ['--proxy-server=http://GATEWAY_HOST:58971'],
});
const page = await browser.newPage();
// Credentials must be set before goto
await page.authenticate({
username: 'USERNAME-country-US-session-job1-time-30',
password: 'PASSWORD',
});
await page.goto('https://api.ipify.org?format=json');
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
Selenium (Python)
Authenticated proxies are the most awkward part of Selenium 4. Three approaches, best to worst:
Option 1: Selenium Wire (recommended)
pip install selenium-wire
from seleniumwire import webdriver
PROXY = "http://USERNAME-country-US-session-job1-time-30:PASSWORD@GATEWAY_HOST:58971"
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(
options=options,
seleniumwire_options={
"proxy": {"http": PROXY, "https": PROXY, "no_proxy": "localhost,127.0.0.1"},
},
)
try:
driver.get("https://api.ipify.org?format=json")
print(driver.find_element("tag name", "body").text)
finally:
driver.quit()
Option 2: Inject credentials via an extension
To avoid pulling in Selenium Wire, generate a Chrome extension on the fly:
import os
import tempfile
import zipfile
from selenium import webdriver
MANIFEST = """
{
"version": "1.0.0",
"manifest_version": 3,
"name": "NextProxy Auth",
"permissions": ["proxy", "webRequest", "webRequestAuthProvider"],
"host_permissions": ["<all_urls>"],
"background": {"service_worker": "background.js"}
}
"""
BACKGROUND = """
chrome.proxy.settings.set({
value: {
mode: 'fixed_servers',
rules: {
singleProxy: { scheme: 'http', host: '%(host)s', port: %(port)d },
bypassList: ['localhost', '127.0.0.1']
}
},
scope: 'regular'
});
chrome.webRequest.onAuthRequired.addListener(
() => ({ authCredentials: { username: '%(user)s', password: '%(password)s' } }),
{ urls: ['<all_urls>'] },
['blocking']
);
"""
def build_auth_extension(host: str, port: int, user: str, password: str) -> str:
"""Build a temporary extension carrying the proxy credentials; returns the crx path."""
path = os.path.join(tempfile.mkdtemp(), "np-auth.zip")
with zipfile.ZipFile(path, "w") as archive:
archive.writestr("manifest.json", MANIFEST)
archive.writestr("background.js", BACKGROUND % {
"host": host, "port": port, "user": user, "password": password,
})
return path
options = webdriver.ChromeOptions()
options.add_extension(build_auth_extension(
"GATEWAY_HOST", 58971,
"USERNAME-country-US-session-job1-time-30", "PASSWORD",
))
driver = webdriver.Chrome(options=options)
try:
driver.get("https://api.ipify.org?format=json")
print(driver.find_element("tag name", "body").text)
finally:
driver.quit()
Option 3: Passwordless ports (least effort)
If your egress IP is fixed, ports from the Extract API need no credentials and Selenium can point straight at them:
import requests
from selenium import webdriver
def fetch_one_port(api_key: str) -> str:
response = requests.get(
"https://api.example.com/api/v1/proxy/extract",
params={"apikey": api_key, "num": 1, "type": "json",
"country": "US", "session": "sticky", "time": 30},
timeout=15,
)
response.raise_for_status()
item = response.json()["data"][0]
return f"{item['ip']}:{item['port']}"
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
# Passwordless port, so no credential problem to solve
options.add_argument(f"--proxy-server=http://{fetch_one_port('YOUR_API_KEY')}")
driver = webdriver.Chrome(options=options)
try:
driver.get("https://api.ipify.org?format=json")
print(driver.find_element("tag name", "body").text)
finally:
driver.quit()
Selenium (Java)
import java.util.HashMap;
import java.util.Map;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
public class SeleniumProxyExample {
public static void main(String[] args) {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
// host:port only; Chromium rejects credentials in launch flags
options.addArguments("--proxy-server=http://GATEWAY_HOST:58971");
ChromeDriver driver = new ChromeDriver(options);
try {
// Supply credentials via CDP to avoid the auth dialog
Map<String, Object> headers = new HashMap<>();
String credentials = java.util.Base64.getEncoder().encodeToString(
"USERNAME-country-US:PASSWORD".getBytes());
headers.put("Proxy-Authorization", "Basic " + credentials);
driver.executeCdpCommand("Network.enable", new HashMap<>());
driver.executeCdpCommand("Network.setExtraHTTPHeaders",
Map.of("headers", headers));
driver.get("https://api.ipify.org?format=json");
System.out.println(driver.findElement(
org.openqa.selenium.By.tagName("body")).getText());
} finally {
driver.quit();
}
}
}
Resource usage and concurrency
Browser automation moves an order of magnitude more traffic than plain HTTP requests — a modern page's first paint can easily be several megabytes. That matters a lot on per-gigabyte dynamic residential billing.
A common way to cut traffic:
# Playwright: block images, fonts and media
def block_heavy_resources(route):
if route.request.resource_type in {"image", "media", "font"}:
route.abort()
else:
route.continue_()
context.route("**/*", block_heavy_resources)
This usually saves 60–80% of the traffic.
Troubleshooting checklist
| Symptom | Cause |
|---|---|
| Browser shows an authentication dialog | Credentials placed in --proxy-server, which Chromium ignores |
| Extension approach fails headless | Chrome's headless mode has incomplete extension support; use Selenium Wire |
| Page load timeouts | The gateway idles out at 5 minutes but the browser may wait longer; set page.set_default_timeout() explicitly |
| Flagged for a mismatch between exit IP and browser timezone | Set locale / timezone_id to match |
| Every page shows the same IP | Puppeteer's proxy is browser-wide; use Playwright's per-context proxies |
400 / 402 / 403 / 407 / 429 / 5xx | See the error reference |