A Playwright proxy is one proxy option. Pass it to browserType.launch() for the whole browser, or to browser.newContext() (Python: browser.new_context()) for one isolated context. It works the same in Chromium, Firefox, and WebKit, in Node.js and Python. The object has four fields: server, bypass, username, and password. Only server is required.
Quick answer:
- Whole browser:
chromium.launch({ proxy: { server, username, password } }) - One context:
browser.newContext({ proxy: { server, username, password } }) - Python: the same dict goes to
p.chromium.launch(proxy={...})orbrowser.new_context(proxy={...}) - Firefox and WebKit: swap
chromiumforfirefoxorwebkit. The option does not change. - Credentials go in
usernameandpassword. Do not put them in theserverURL. - SOCKS5 works, but not with a username and password. Playwright rejects that combination.
This page is a reference for the exact API, with Node.js and Python side by side. The field descriptions match the Playwright BrowserType API docs and the Browser.newContext docs, checked against the docs in October 2026. If you are comparing browser tools, the companion guides cover Puppeteer proxy setup and Python Selenium proxy setup.

Playwright proxy option for Chromium, Firefox, and WebKit
The proxy option is defined on browserType.launch() and on browser.newContext(). The official docs list one shape for every engine and do not split it by browser. So the only change between engines is the browser type you launch.
Node.js, the same proxy on all three engines:
import { chromium, firefox, webkit } from "playwright";
const proxy = {
server: "http://proxy.example.com:8080",
username: process.env.PROXY_USERNAME,
password: process.env.PROXY_PASSWORD,
};
for (const browserType of [chromium, firefox, webkit]) {
const browser = await browserType.launch({ proxy });
const page = await browser.newPage();
await page.goto("https://example.com");
console.log(browserType.name(), await page.title());
await browser.close();
}
Python, sync API:
import os
from playwright.sync_api import sync_playwright
proxy = {
"server": "http://proxy.example.com:8080",
"username": os.environ["PROXY_USERNAME"],
"password": os.environ["PROXY_PASSWORD"],
}
with sync_playwright() as p:
for browser_type in (p.chromium, p.firefox, p.webkit):
browser = browser_type.launch(proxy=proxy)
page = browser.new_page()
page.goto("https://example.com")
print(browser_type.name, page.title())
browser.close()
Python, async API:
import asyncio
import os
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(proxy={
"server": "http://proxy.example.com:8080",
"username": os.environ["PROXY_USERNAME"],
"password": os.environ["PROXY_PASSWORD"],
})
context = await browser.new_context()
page = await context.new_page()
await page.goto("https://example.com")
print(await page.title())
await browser.close()
asyncio.run(main())
Engine differences show up at the edges, not in the option. Loopback and localhost handling varies by engine, and Playwright's own test suite skips one bypass test on WebKit for macOS because that build does not proxy localhost when bypass rules are set. If a proxy test passes in Chromium and fails in another engine, check localhost and bypass first.
Playwright proxy quick setup (Node.js and Python)
Node.js, one proxy for the whole browser:
import { chromium } from "playwright";
const browser = await chromium.launch({
proxy: {
server: "http://proxy.example.com:8080",
username: process.env.PROXY_USERNAME,
password: process.env.PROXY_PASSWORD,
},
});
const page = await browser.newPage();
await page.goto("https://example.com");
console.log(await page.title());
await browser.close();
Python, same setup with the sync API:
import os
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(proxy={
"server": "http://proxy.example.com:8080",
"username": os.environ["PROXY_USERNAME"],
"password": os.environ["PROXY_PASSWORD"],
})
page = browser.new_page()
page.goto("https://example.com")
print(page.title())
browser.close()
Every page and context from that browser uses the launch proxy, unless a context sets its own proxy.
Playwright proxy option fields
The proxy object has the same shape everywhere Playwright accepts it. In Node.js it is an object. In Python it is a dict with the same keys.
| Field | Type | Required | What it does |
|---|---|---|---|
server |
string | Yes | Proxy URL, for example http://myproxy.com:3128 or socks5://myproxy.com:3128. A short form like myproxy.com:3128 is treated as HTTP. |
bypass |
string | No | Comma-separated hosts that skip the proxy, for example ".com, chromium.org, .domain.com". |
username |
string | No | Username for an HTTP proxy that requires authentication. |
password |
string | No | Password for an HTTP proxy that requires authentication. |
These are the places where you can pass it:
| Where | Node.js | Python | Scope |
|---|---|---|---|
| Browser launch | browserType.launch({ proxy }) |
browser_type.launch(proxy=...) |
All contexts that do not set their own proxy |
| Persistent profile | browserType.launchPersistentContext(dir, { proxy }) |
browser_type.launch_persistent_context(dir, proxy=...) |
That one persistent context |
| New context | browser.newContext({ proxy }) |
browser.new_context(proxy=...) |
One context and its pages |
| New page shortcut | browser.newPage({ proxy }) |
browser.new_page(proxy=...) |
The page and its hidden context |
| API requests | request.newContext({ proxy }) |
p.request.new_context(proxy=...) |
APIRequestContext calls only, not the browser |
| Playwright Test | use: { proxy } in playwright.config.ts |
browser_context_args fixture in pytest-playwright |
Every test context |
browser_type in Python is p.chromium, p.firefox, or p.webkit.
Per-context proxy with browser.newContext()
A browser context has its own cookies, local storage, cache, and permissions. Give each context its own proxy and you get separate identities inside one browser process. This is the most useful Playwright proxy feature for scraping and multi-account QA.
Node.js:
const browser = await chromium.launch();
const context = await browser.newContext({
proxy: {
server: "http://proxy.example.com:8080",
username: process.env.PROXY_USERNAME,
password: process.env.PROXY_PASSWORD,
},
});
const page = await context.newPage();
await page.goto("https://example.com");
await context.close();
await browser.close();
Python:
browser = p.chromium.launch()
context = browser.new_context(proxy={
"server": "http://proxy.example.com:8080",
"username": os.environ["PROXY_USERNAME"],
"password": os.environ["PROXY_PASSWORD"],
})
page = context.new_page()
page.goto("https://example.com")
context.close()
Rules for per-context proxies:
- A context
proxyoverrides the launchproxyfor that context. - A context without
proxyuses the launch proxy, or a direct connection if the browser has none. - You cannot change a context's proxy after you create it. To switch IPs, close the context and create a new one.
- Older Chromium builds on Windows ignored per-context proxies unless the browser itself was launched with some proxy. The workaround in the Playwright Java docs is a placeholder:
launch({ proxy: { server: "http://per-context" } }). Recent versions should not need it, but if contexts show your real IP on an old Playwright pin, try it.
Playwright proxy authentication (username and password)
Put the endpoint in server and the credentials in username and password. The same three fields work at launch and at context level.
Two details cause most authentication failures:
- Credentials inside
serverare dropped. Playwright normalizesserverto the scheme, host, and port. A value likehttp://user:pass@host:8080losesuser:passwithout an error, and the proxy then answers with 407. - Chromium sends credentials only after a challenge. The browser waits for the proxy to answer
407 Proxy Authentication Requiredwith aProxy-Authenticateheader, then retries withProxy-Authorization. A proxy that expects the header on the first request, without a challenge, will not work withusernameandpassword. Use IP allowlisting on that proxy, or put a local forwarder in front of it.
If your provider gives a four-part string like host:port:username:password, split it like this:
server: "http://host:port"
username: "username"
password: "password"
The proxy formatter converts lists between host:port:user:pass, user:pass@host:port, and other layouts.
Keep one proxy URL in an environment variable
A common pattern is one PROXY_URL secret, such as http://user:pass@host:8080, split into the three Playwright fields in code. The URL class handles the parsing:
function proxyFromUrl(raw: string) {
const url = new URL(raw);
return {
server: `${url.protocol}//${url.host}`,
username: url.username ? decodeURIComponent(url.username) : undefined,
password: url.password ? decodeURIComponent(url.password) : undefined,
};
}
const browser = await chromium.launch({
proxy: proxyFromUrl(process.env.PROXY_URL!),
});
Python does the same with urllib.parse.urlsplit:
import os
from urllib.parse import urlsplit, unquote
def proxy_from_url(raw):
u = urlsplit(raw)
return {
"server": f"{u.scheme}://{u.hostname}:{u.port}",
"username": unquote(u.username) if u.username else None,
"password": unquote(u.password) if u.password else None,
}
proxy = proxy_from_url(os.environ["PROXY_URL"])
Keep credentials out of committed scripts. Load them from environment variables or CI secrets.
When an AI agent drives the browser through Playwright MCP instead of your own script, the credentials go in the MCP server config. The Playwright MCP proxy setup guide shows that format.
SOCKS5 proxies in Playwright
server accepts socks5://host:port. Playwright has two limits here:
- No SOCKS5 authentication. If you set
usernameorpasswordwith asocks5://server, Playwright throwsBrowser does not support socks5 proxy authentication. Forsocks4://with credentials it throwsSocks4 proxy protocol does not support authentication. - DNS goes through the proxy. For a launch-level SOCKS5 proxy in Chromium, Playwright adds a host-resolver rule so the browser does not resolve target hostnames locally.
If your SOCKS5 proxy needs a password, you have three options:
- Use the HTTP endpoint from the same provider. Most proxy providers offer both.
- Allowlist your server IP so the SOCKS5 proxy does not need credentials.
- Run a local forwarder on
127.0.0.1that accepts unauthenticated connections and forwards to the authenticated upstream. Theproxy-chainnpm package can do this. Pointserverat the local port.
A local forwarder with proxy-chain looks like this for an HTTP upstream:
import { chromium } from "playwright";
import { anonymizeProxy, closeAnonymizedProxy } from "proxy-chain";
const upstream = `http://${process.env.PROXY_USERNAME}:${process.env.PROXY_PASSWORD}@proxy.example.com:8080`;
const local = await anonymizeProxy(upstream); // http://127.0.0.1:<random port>
const browser = await chromium.launch({ proxy: { server: local } });
// ... run your task ...
await browser.close();
await closeAnonymizedProxy(local, true);
For a protocol comparison, read SOCKS5 vs HTTP proxy.
Bypass rules and localhost
bypass takes a comma-separated string of hosts that should connect directly:
const browser = await chromium.launch({
proxy: {
server: "http://proxy.example.com:8080",
bypass: "localhost,127.0.0.1,.internal.example.com",
},
});
A leading dot matches subdomains, so .internal.example.com covers api.internal.example.com.
Watch out for loopback. Plain Chrome skips the proxy for localhost by default. Playwright's Chromium does the opposite: it sends loopback traffic through the proxy unless bypass lists localhost, 127.0.0.1, ::1, or <loopback>. If your tests hit a local dev server and fail only when the proxy is on, add localhost to bypass. The official docs do not describe loopback behavior, so verify it on your engine with a request to your dev server.
Use bypass rules sparingly. If you bypass a target by accident, your own IP reaches the site.
Environment proxy variables (HTTP_PROXY, HTTPS_PROXY)
Playwright has no documented option that makes page traffic follow HTTP_PROXY or HTTPS_PROXY. Set proxy explicitly and read the values yourself:
const browser = await chromium.launch({
proxy: process.env.PROXY_SERVER
? {
server: process.env.PROXY_SERVER,
username: process.env.PROXY_USERNAME,
password: process.env.PROXY_PASSWORD,
}
: undefined,
});
HTTPS_PROXY does matter in one place. npx playwright install and playwright install use it to download browsers from behind a corporate proxy, as described in the Playwright browsers docs.
Playwright Test proxy config
For Playwright Test, put a shared proxy in playwright.config.ts. This is the global form from the official network docs:
import { defineConfig } from "@playwright/test";
export default defineConfig({
use: {
proxy: {
server: "http://proxy.example.com:8080",
username: process.env.PROXY_USERNAME,
password: process.env.PROXY_PASSWORD,
},
},
});
You can also set use.proxy per project, for example one project per region. For one test file, test.use({ proxy: { server: "..." } }) overrides the config.
With pytest-playwright, override the browser_context_args fixture:
import os
import pytest
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
return {
**browser_context_args,
"proxy": {
"server": "http://proxy.example.com:8080",
"username": os.environ["PROXY_USERNAME"],
"password": os.environ["PROXY_PASSWORD"],
},
}
For scraping jobs with many identities, create contexts in your own worker code. That is clearer than hiding rotation inside test config.
Rotating proxies per browser context
The safest rotation unit is a browser context. Create a context for each proxy identity, run one bounded task, then close it:
import { chromium, type BrowserContextOptions } from "playwright";
const proxies: BrowserContextOptions["proxy"][] = [
{ server: "http://proxy-1.example.com:8080" },
{ server: "http://proxy-2.example.com:8080" },
{ server: "http://proxy-3.example.com:8080" },
];
const browser = await chromium.launch();
for (const proxy of proxies) {
const context = await browser.newContext({ proxy });
const page = await context.newPage();
try {
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
// Run one bounded workflow for this proxy identity.
} finally {
await context.close();
}
}
await browser.close();
The Python version:
proxies = [
{"server": "http://proxy-1.example.com:8080"},
{"server": "http://proxy-2.example.com:8080"},
{"server": "http://proxy-3.example.com:8080"},
]
browser = p.chromium.launch()
for proxy in proxies:
context = browser.new_context(proxy=proxy)
page = context.new_page()
try:
page.goto("https://example.com", wait_until="domcontentloaded")
# Run one bounded workflow for this proxy identity.
finally:
context.close()
browser.close()
Cookies, storage, and IP stay together this way. If the target expects the same visitor across a login, cart, checkout, or account flow, do not rotate between steps. Use a sticky residential session or a stable ISP proxy for the whole flow. Our guide to sticky vs rotating sessions explains when each fits. If your provider exposes one rotating gateway, the backconnect proxy guide explains why a fresh context on the same server can still get a new IP.
Retry on a different proxy
Proxies fail. Wrap each task so a failed attempt closes its context and the next attempt takes the next proxy:
import type { Browser, BrowserContextOptions } from "playwright";
type Proxy = NonNullable<BrowserContextOptions["proxy"]>;
async function loadWithRetries(browser: Browser, url: string, proxies: Proxy[], attempts = 3) {
let lastError: unknown;
for (let i = 0; i < attempts; i++) {
const context = await browser.newContext({ proxy: proxies[i % proxies.length] });
try {
const page = await context.newPage();
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 });
return await page.content();
} catch (error) {
lastError = error;
} finally {
await context.close();
}
}
throw lastError;
}
Retry on connection errors and timeouts. Do not retry a 403 or 429 on the same URL in a tight loop, because that adds load to a site that already rejected you.
Playwright can open pages fast enough to overload a site or your proxy pool. Use a queue with a concurrency limit, not unbounded parallel pages.

Common Playwright proxy errors
The net::ERR_* names below come from Chromium. Firefox and WebKit report the same failures with different messages.
| Error | What it means | Fix |
|---|---|---|
net::ERR_PROXY_CONNECTION_FAILED |
The browser could not open a TCP connection to the proxy | Check host, port, and firewall. Test with curl -x. See our ERR_PROXY_CONNECTION_FAILED guide. |
net::ERR_TUNNEL_CONNECTION_FAILED |
The proxy refused the HTTPS CONNECT tunnel |
Check that the proxy allows the target host and port 443, that the scheme is right, and that your plan has not expired |
407 or net::ERR_INVALID_AUTH_CREDENTIALS |
The proxy rejected or did not get credentials | Use username and password fields, not the server URL. Check for typos. See 407 Proxy Authentication Required. |
Browser does not support socks5 proxy authentication |
You set credentials on a socks5:// server |
Use an HTTP endpoint, IP allowlisting, or a local forwarder |
net::ERR_SOCKS_CONNECTION_FAILED |
The SOCKS handshake failed | Confirm the port really speaks SOCKS5, not HTTP |
| Contexts show your real IP on Chromium/Windows | Older versions ignored per-context proxies without a launch proxy | Upgrade Playwright, or launch with proxy: { server: "http://per-context" } |
| Page shows your real IP | The context has no proxy, or the host matches bypass |
Log the proxy per context and check an IP echo page |
page.goto timeout |
Slow or overloaded proxy, or the target hangs | Test the same proxy with curl, then raise timeout only after that works |
To confirm the proxy is active, load an IP echo page in the same context:
await page.goto("https://ipv4.unknownproxies.com/ip");
console.log(await page.textContent("body"));
Then debug one layer at a time:
- Test the proxy with
curl -x http://user:pass@host:port https://ipv4.unknownproxies.com/ip. - Launch one browser with one proxy and check the IP echo page.
- Load a normal HTTPS page.
- Try headed mode (
headless: false) to see auth prompts or error pages. - Compare the same target without the proxy.
If the target returns HTTP 403 Forbidden or HTTP 429 Too Many Requests, the proxy is usually working. The site received the traffic and rejected it based on rules, rate limits, or session signals.
Concurrency and proxy bandwidth
Each Playwright page loads scripts, images, API calls, and tracking endpoints. A single news homepage can transfer 2-5 MB, which is why proxy bandwidth, not browser time, usually decides the bill when you compare Browserbase alternatives against running Playwright yourself. Blocking images and fonts with page.route() cuts that.
Good defaults:
- One context per proxy identity.
- A limit on concurrent contexts per proxy.
- Sticky sessions for multi-step journeys. Rotate between independent tasks, not mid-session.
- Jitter and backoff after failures.
- Logs with proxy ID, target host, status, and retry count.
If an LLM drives Playwright instead of a fixed script, read proxies for AI agents for how to map one agent task to one proxy session. For Python agents, the Browser Use agent proxy guide covers ProxySettings and one proxy identity per agent.
Residential vs ISP proxies for Playwright
Residential proxies fit Playwright workflows that need consumer IPs, many locations, or rotating sessions for independent public pages. ISP proxies fit workflows that need speed and the same dedicated IP over time, such as QA, account flows, checkout testing, and monitoring. For retail and sneaker checkout flows, where a mid-session IP change can end the session, the sneaker proxy guide compares both types.
Neither replaces consistent browser behavior. Headers, cookies, viewport, timing, and request volume still matter.
Unknown Proxies offers residential proxies with country and state targeting, and dedicated ISP proxy plans. Both work as HTTP proxies with username and password in Playwright.
FAQ
How do I set a proxy in Playwright?
Pass a proxy object to browserType.launch() for the whole browser, or to browser.newContext() for one context. In Python, pass a dict to p.chromium.launch(proxy=...) or browser.new_context(proxy=...). The object needs a server and can include username, password, and bypass.
What does the proxy option of browser.new_context accept in Python?
A dict with server (required) and optional bypass, username, and password, all strings. For example: browser.new_context(proxy={"server": "http://host:8080", "username": "user", "password": "pass"}).
How do I pass a proxy username and password to browserType.launch()?
Use separate fields: launch({ proxy: { server: "http://host:port", username: "user", password: "pass" } }). Do not embed user:pass@ in server, because Playwright drops it.
Does Playwright support proxies in Firefox and WebKit?
Yes. The proxy option has the same shape for chromium, firefox, and webkit, at launch and per context. Engine differences appear mostly in error messages and in how localhost is handled with bypass.
Can I use a different proxy per browser context?
Yes. Each browser.newContext({ proxy }) call can use a different proxy. The context proxy overrides the launch proxy for that context. You cannot change it later, so create a new context to switch.
Does Playwright support SOCKS5 proxies with authentication?
No. Playwright supports socks5:// servers without credentials. With username or password it throws Browser does not support socks5 proxy authentication. Use an HTTP endpoint or a local forwarder.
How do I rotate proxies in Playwright?
Create a new browser context with a different proxy for each task, then close it when the task ends. Keep one proxy for the whole session when the target tracks login, cart, or checkout state.
Final thoughts
Match proxy identity to browser context identity. Use a launch proxy for one simple route, newContext({ proxy }) for isolated sessions, and sticky sessions when the target expects continuity. Keep credentials in username and password, avoid authenticated SOCKS5, and treat 403 or 429 as target behavior, not proof that the proxy setting failed.
Technical references: Playwright network proxy docs, Playwright Python network docs, and Chromium proxy support notes.