We use cookies to enhance user experience, personalize content, and analyze traffic. Cookie Policy

← Back to all articles

Playwright Proxy: Per-Context Setup and Authentication

Set a Playwright proxy in launch() or per browser context with newContext() / new_context(). Covers server, bypass, username, password, SOCKS5, and errors.

by Unknown Proxies

15 min read

June 18, 2026

Updated October 17, 2026

Playwright Proxy: Per-Context Setup and Authentication

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:

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 architecture with one browser and multiple isolated contexts using different proxies

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:

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:

  1. Credentials inside server are dropped. Playwright normalizes server to the scheme, host, and port. A value like http://user:pass@host:8080 loses user:pass without an error, and the proxy then answers with 407.
  2. Chromium sends credentials only after a challenge. The browser waits for the proxy to answer 407 Proxy Authentication Required with a Proxy-Authenticate header, then retries with Proxy-Authorization. A proxy that expects the header on the first request, without a challenge, will not work with username and password. 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:

If your SOCKS5 proxy needs a password, you have three options:

  1. Use the HTTP endpoint from the same provider. Most proxy providers offer both.
  2. Allowlist your server IP so the SOCKS5 proxy does not need credentials.
  3. Run a local forwarder on 127.0.0.1 that accepts unauthenticated connections and forwards to the authenticated upstream. The proxy-chain npm package can do this. Point server at 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.

Playwright proxy rotation checklist covering context creation, sticky sessions, pacing, and logging

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:

  1. Test the proxy with curl -x http://user:pass@host:port https://ipv4.unknownproxies.com/ip.
  2. Launch one browser with one proxy and check the IP echo page.
  3. Load a normal HTTPS page.
  4. Try headed mode (headless: false) to see auth prompts or error pages.
  5. 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:

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.

About the Author

Unknown Proxies

Proxy Infrastructure Team

Stay Unknown

High-performance dedicated proxies optimized for speed and reliability. Get uncompromising quality, 99.9% uptime, and unmatched support. Stay Unknown.

Explore Plans