Guide

How to Use Proxies With Playwright (Chromium, Firefox, WebKit)

How to use proxies with Playwright: per-context proxy config, the credentials-in-URL trap, rotation with sticky sessions, and verifying your real exit IP.

HProxy Team · ·Updated July 21, 2026 ·10 min read
HProxy. Guide

Free proxies won't hold up here.

Shared datacenter IPs get flagged and dropped fast. When it has to hold, gaming, streaming, accounts, you need mobile and residential IPs that read as a real device, from $0.65/GB, pay as you go.

See plans & pricing

Playwright makes proxies easy right up to the moment you paste in a username and password, at which point it quietly ignores them. Routing a Playwright browser through a proxy is a two line change. What trips people up is where the proxy goes (at launch, or per browser context) and why credentials embedded in the proxy URL vanish with no error at all. This guide covers proxies in Playwright end to end: per-context configuration across Chromium, Firefox and WebKit, the credentials trap and its one field fix, rotation with sticky sessions, and confirming the exit IP a target actually sees.

We run a proxy network and a live proxy checker, so we watch people hit the same two walls: the silent 407 from credentials in the wrong place, and a rotation loop that leaks the last session onto the next IP because the context was never closed. Every example below is runnable. Where one needs a live proxy, pull a fresh one from our free proxy API, which returns real, recently checked endpoints with no key.

How do you set a proxy in Playwright?

Pass a proxy object with a server field, either globally to chromium.launch() or, better, per context to browser.newContext(). Add username and password as their own fields if the proxy needs authentication, and never inside the server URL. The proxy then covers every page in that browser or context.

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({
    proxy: { server: 'http://proxy.example.com:8080', username: 'user', password: 'pass' },
  });
  const page = await browser.newPage();
  await page.goto('https://httpbin.org/ip');   // shows the exit IP the site sees
  console.log(await page.textContent('body'));
  await browser.close();
})();

Set the proxy at launch, or per context

A proxy set on launch() applies to the whole browser: every context, every page. That is the simplest setup and the right one when the whole job runs through a single exit.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(
        proxy={"server": "http://proxy.example.com:8080", "username": "user", "password": "pass"}
    )
    page = browser.new_page()
    page.goto("https://httpbin.org/ip")
    print(page.text_content("body"))
    browser.close()

The more useful form sets the proxy per browser context. A context is an isolated session: its own cookies, its own local storage, its own cache. Give each context its own proxy and you can run two identities, on two exit IPs, inside one browser process at the same time.

const browser = await chromium.launch();

const ctxUS = await browser.newContext({
  proxy: { server: 'http://us.proxy.example.com:8080', username: 'user', password: 'pass' },
});
const ctxDE = await browser.newContext({
  proxy: { server: 'http://de.proxy.example.com:8080', username: 'user', password: 'pass' },
});

await (await ctxUS.newPage()).goto('https://httpbin.org/ip');   // one exit
await (await ctxDE.newPage()).goto('https://httpbin.org/ip');   // a different exit, same browser

Per-context proxying is the foundation of every rotation and sticky-session pattern below, so reach for newContext by default and keep launch-level proxies for the single-exit case.

The credentials trap: why user:pass in the URL disappears

Here is the wall almost everyone hits. Most paid proxies want a username and password, and the obvious place to put them is the URL, the way curl and Python requests accept http://user:pass@host:port. In Playwright that silently fails. Under the hood Playwright launches Chromium with a --proxy-server flag, and that flag carries no credentials, so the user:pass you wrote into server is thrown away. The proxy then answers 407 Proxy Authentication Required and your navigation dies with no obvious cause.

// WRONG: Chromium's proxy flag drops the credentials from the URL, so the proxy returns 407
const context = await browser.newContext({
  proxy: { server: 'http://user:[email protected]:8080' },
});

// RIGHT: credentials live in their own fields, and Playwright answers the proxy's auth challenge
const context = await browser.newContext({
  proxy: { server: 'http://proxy.example.com:8080', username: 'user', password: 'pass' },
});

If you have worked through our Selenium proxy guide, this is the same gotcha with a much better ending. Selenium's Chrome flag drops credentials too, and the fix there is a third-party tool, selenium-wire, that runs a local proxy to inject the auth. Playwright folds that job into the API: the username and password fields make it answer the proxy's authentication challenge for you, no extra dependency, no local shim in the request path.

The same rule reaches beyond Chromium. It is a Playwright convention, not a browser quirk, so credentials belong in the fields for Firefox and WebKit too.

Chromium, Firefox and WebKit

The proxy object is identical across all three engines Playwright drives. Swap chromium for firefox or webkit and the same { server, username, password } shape works, including per context.

from playwright.sync_api import sync_playwright

proxy = {"server": "http://proxy.example.com:8080", "username": "user", "password": "pass"}

with sync_playwright() as p:
    for engine in (p.chromium, p.firefox, p.webkit):
        browser = engine.launch(proxy=proxy)
        page = browser.new_page()
        page.goto("https://httpbin.org/ip")
        print(engine.name, page.text_content("body"))
        browser.close()

Two engine-level details are worth keeping in mind.

CapabilityChromiumFirefoxWebKit
HTTP or HTTPS proxyYesYesYes
Credentials via username / password fieldsYesYesYes
SOCKS5 without auth (socks5://host:1080)YesYesYes
SOCKS5 with username and passwordNoNoNo

SOCKS5 authentication is not supported. Playwright's credential fields are documented for HTTP and HTTPS proxies. A socks5:// server works, but only unauthenticated, so if your SOCKS5 exit needs a login you have to use its HTTP endpoint instead or authenticate by allowlisting your machine's IP with the provider. The bypass field, a comma-separated list like "localhost, .internal.example.com", lets specific hosts skip the proxy entirely.

Older Playwright and Chromium per-context proxies. Current Playwright applies a per-context proxy on Chromium directly. Older releases required launching the browser with a placeholder proxy (server: 'per-context') before context-level proxies would take effect. If a per-context proxy seems ignored on Chromium, upgrade Playwright before you debug anything else, because that one requirement caused a lot of confusion for no reason.

Rotate proxies across contexts

Because each context carries its own exit and its own cookies, rotation is just a loop that opens a fresh context per proxy and closes it when the identity's work is done.

const { chromium } = require('playwright');

const POOL = [
  { server: 'http://p1.example.com:8080', username: 'user', password: 'pass' },
  { server: 'http://p2.example.com:8080', username: 'user', password: 'pass' },
  { server: 'http://p3.example.com:8080', username: 'user', password: 'pass' },
];

(async () => {
  const browser = await chromium.launch();
  for (const proxy of POOL) {
    const context = await browser.newContext({ proxy });   // fresh identity + exit
    const page = await context.newPage();
    try {
      const res = await page.goto('https://httpbin.org/ip', { timeout: 20000 });
      console.log(proxy.server, '->', (await res.json()).origin);
      // ... your work for this identity ...
    } catch (err) {
      console.log(proxy.server, 'failed:', err.message);   // dead or blocked exit, move on
    } finally {
      await context.close();   // drops cookies + storage so the next IP starts clean
    }
  }
  await browser.close();
})();

The load-bearing line is context.close() in the finally. Skip it and the cookies from one exit linger in the browser, so the next IP shows up already carrying the last session's state, which is exactly the pairing that gets a rotation run flagged. Closing the context is what makes each new IP a genuinely fresh identity.

For most jobs you do not want to manage a list at all. Point every context at one rotating residential gateway, a single host and port, and let the pool hand you a different exit on each connection. Your code stays a plain one-proxy script and the rotation happens upstream.

gateway = {"server": "http://gateway.example.com:8080", "username": "user", "password": "pass"}
context = browser.new_context(proxy=gateway)   # a fresh exit per connection, no list to maintain

Sticky sessions versus per-request rotation

Rotating hard on every request is the wrong instinct against a defended target, and it is worth understanding why. Once you clear a login or a bot challenge, the site hands you a cookie tied to the IP that earned it. Change IP on the next request and that cookie no longer matches its origin, so you get re-challenged as a stranger. A single context on a single exit is already a sticky session: hold it for the whole of one flow.

// One context = one exit, held for the length of a browsing flow
const context = await browser.newContext({
  proxy: { server: 'http://gateway.example.com:8080', username: 'user', password: 'pass' },
});
const page = await context.newPage();
await page.goto('https://example.com/login');       // sets a cookie tied to this exit
await page.goto('https://example.com/dashboard');   // same context, same IP, cookie still valid

The rule is simple: rotate between flows, not inside them. New identity means a new context and a new IP; one identity means one context held from start to finish. We go deep on why this beats machine-gun rotation, with the same reasoning applied to a real bot wall, in how to get around DataDome.

Verify the exit IP

Never assume a proxy is working. Confirm the target sees the exit's address and not yours, per context, before you trust a run.

async function exitIp(context) {
  const page = await context.newPage();
  const res = await page.goto('https://httpbin.org/ip');
  const { origin } = await res.json();
  await page.close();
  return origin;
}

const direct = await exitIp(await browser.newContext());
const viaProxy = await exitIp(await browser.newContext({
  proxy: { server: 'http://proxy.example.com:8080', username: 'user', password: 'pass' },
}));
console.log('direct:', direct, 'proxy:', viaProxy);
if (direct === viaProxy) throw new Error('proxy is not changing your IP');

If the two addresses match, the proxy is not routing your traffic, which usually means the credentials landed in the server URL instead of their own fields and the context fell back to a direct connection. It is also worth a look at the exit's real geography and anonymity grade, not just that the IP changed. Our proxy checker runs the whole battery (exit IP, anonymity, real geolocation, latency) in one paste, which is faster than scripting each check by hand.

A proxy fixes the IP, not the fingerprint

This is the honest ceiling, and it is where Playwright scrapers waste the most time. A proxy changes the address a site sees. It does nothing about the other signals. Playwright wins the hardest layer for free, because its TLS and HTTP/2 handshake genuinely is a real browser's: it genuinely is Chromium, Firefox or WebKit. But a default automated session still sets navigator.webdriver to true, exposes automation artifacts, and moves with a precision no hand produces. A serious anti-bot system reads all of that no matter how clean your exit IP is.

We wrote the full breakdown from the defender's seat in how websites detect proxies: the network the IP belongs to, the TLS and HTTP/2 fingerprint, header order, request pacing, and DNS or WebRTC leaks. Hardened Playwright variants and human-like pacing handle the parts a proxy cannot, and the two big vendor walls have their own playbooks in scraping past Cloudflare and getting around DataDome. Set expectations accordingly: a real browser on a good residential IP gets you a long way, and it is not a magic cloak.

Where to point Playwright

For anything facing real defenses the exit IP decides the most, so start there. Our residential proxies route through real consumer ISP connections and rotate from a single gateway, which is the exact shape Playwright wants: one host and port in your proxy object, a fresh believable IP per context or per connection, and sticky sessions when you need to hold one identity across a flow. Pricing is $0.65/GB, pay as you go, and your balance never expires, so a scraping job that idles for a week costs nothing while it waits.

Before you scale, prove one identity end to end: verify the exit with the proxy checker, confirm the fingerprint and pacing on a single page, and only then fan out. If you are driving Playwright from inside a larger crawl, the framework-scale sibling to this guide is how to use proxies with Scrapy, which covers rotation, retries and proxy auth at the pipeline level, including a note on running Playwright as a Scrapy download handler.

Sources

Frequently asked questions

How do I set a proxy in Playwright?
Pass a proxy object when you launch the browser or, better, when you create a context: browser.newContext({ proxy: { server: 'http://host:port' } }). Add username and password fields if the proxy needs authentication. The proxy then applies to every page in that context. Setting it per context, rather than globally at launch, is what lets you run different exits in the same browser.
Why is my Playwright proxy username and password ignored?
Because you put them in the server URL. If you write server: 'http://user:pass@host:port', Chromium's proxy flag drops the user:pass part and the proxy answers 407 Proxy Authentication Required. Playwright takes credentials only from the separate username and password fields of the proxy object. Move them there and the auth goes through.
Can I use a different proxy per browser context in Playwright?
Yes, and it is the cleanest way to rotate. Each call to browser.newContext({ proxy: ... }) gets its own exit IP plus its own cookies and storage, so two contexts in one browser can wear two different identities at once. Close a context when you are done with an identity so its cookies do not follow the next IP.
Does Playwright support SOCKS5 proxies?
Yes, set server: 'socks5://host:1080'. The catch is authentication: Playwright supports SOCKS5 only without a username and password. If your SOCKS5 proxy needs credentials, it will not work, so use an HTTP proxy (which does support the username and password fields) or authenticate that exit by IP allowlist instead.
How do I rotate proxies in Playwright?
Either create a new context per identity, each with a different proxy from your pool, or point every context at one rotating residential gateway and let the pool hand you a fresh IP per connection. The gateway approach keeps your code a plain one-proxy script and moves the rotation upstream, which is usually less to maintain.
Does a proxy stop Playwright from being detected?
No. A proxy fixes the IP a site sees and nothing else. Playwright still drives a real browser whose TLS fingerprint genuinely is Chrome's or Firefox's, which helps, but a default automated session still leaks navigator.webdriver and moves with machine precision. A clean residential IP is necessary against a serious anti-bot system, not sufficient.

Proxies that don't die mid-job

Residential, ISP, datacenter and mobile, verified by the same engine that runs tens of millions of checks. They read as a real device and hold up under load. Pay as you go, and your balance never expires.

47M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup