Playwright takes a proxy through the proxy option, either on browserType.launch() (applies to the whole browser) or on browser.newContext() (applies to one context). The object accepts server, username, password and bypass. Everything else, rotation, geo targeting, session length, is controlled by the proxy credentials you pass, not by Playwright.
Proxy at browser launch
The simplest form. Every context and page in this browser exits through the same proxy.
const { chromium } = require('playwright');
const browser = await chromium.launch({
proxy: {
server: 'http://gate.example.net:8000',
username: 'user_c_US',
password: 'pass',
},
});
const page = await browser.newPage();
await page.goto('https://example.com');
Python is identical in shape:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(proxy={
"server": "http://gate.example.net:8000",
"username": "user_c_US",
"password": "pass",
})
page = browser.new_page()
page.goto("https://example.com")
In a Playwright Test project, put the same object under use.proxy in playwright.config.js so it applies to every test worker.
Per-context proxies: several proxies in one browser
This is what most scraping and multi-account work actually needs. Each BrowserContext gets its own cookie jar, storage and proxy, so one browser process can run several isolated identities.
const browser = await chromium.launch({
proxy: { server: 'http://per-context' }, // placeholder, see below
});
const us = await browser.newContext({
proxy: { server: 'http://gate.example.net:8000', username: 'user_c_US_s_a1_ttl_30m', password: 'pass' },
});
const uk = await browser.newContext({
proxy: { server: 'http://gate.example.net:8000', username: 'user_c_GB_s_b2_ttl_30m', password: 'pass' },
});
The configuration detail that costs people an afternoon: in Chromium, context-level proxies only take effect if the browser was launched with a proxy option present. A placeholder such as { server: 'http://per-context' } is enough, and Playwright's own docs recommend exactly this pattern. Launch Chromium with no proxy at all and your newContext proxy is silently ignored on some platforms, so your requests leave from the host IP. Firefox honours per-context proxies without the placeholder, which is why the bug tends to surface only after you switch browsers.
Also note that per-context proxying does not apply to browser.newPage() shortcuts you created before the contexts, and launchPersistentContext() takes the proxy in its single options object, not twice.
Authentication and bypass
Credentials belong in the proxy object, not in the URL. Playwright does support http://user:pass@host:port in server, but the parsed form is easier to template and avoids URL-encoding bugs when a password contains @ or #.
proxy: {
server: 'http://gate.example.net:8000',
username: 'user',
password: 'pass',
bypass: 'localhost,127.0.0.1,*.internal',
}
bypass is a comma-separated list of hosts that skip the proxy. Use it for your own health-check endpoints so they do not burn proxy traffic.
Two limits worth knowing before you pick a protocol:
- SOCKS5 with authentication is not supported in Chromium. If you need username and password auth on Chromium, use HTTP. SOCKS5 works with unauthenticated setups or IP whitelisting. The trade-offs between protocols are covered in SOCKS5 vs HTTP vs HTTPS.
- Proxy settings do not cover WebRTC. A browser can still expose the host address over WebRTC even when HTTP traffic is proxied correctly, so test it with the WebRTC leak test before you trust the setup.
Rotation: per request, sticky, or per context
Playwright has no rotation feature. Rotation is a property of the proxy endpoint, and you have three usable patterns:
| Pattern | How you set it | Use it for |
|---|---|---|
| Per-request rotation | Pool credential with no session parameter; a new exit IP on each request | Independent page fetches, public listing pages, price sampling |
| Sticky session | Add a session ID and TTL to the proxy username, one ID per context | Logins, carts, multi-step flows, anything stateful |
| Dedicated device | One device per customer, rotated on demand via API or timer | Long-lived accounts that must keep a stable identity |
On VoidMob's shared pool you chain underscore parameters onto the proxy username, which fits Playwright cleanly because the username is just a string you template per context: user_c_US_s_ctx7_ttl_30m pins every request from that context to the same US device for 30 minutes. Drop the _s_ parameter and you get per-request rotation. Change the session string and you get a new device. Named proxy lists do the same thing through configuration if you would rather not build usernames in code.
The rule that matters: one session ID per browser context, never per page. Two contexts sharing a session ID means two cookie jars exiting from one IP, which is the pattern platforms read as one operator running several accounts. If sticky windows are new to you, what a sticky session is covers window length by workload.
Verify the exit IP before you run volume
Fastest sanity check: navigate to an IP echo service inside the context and log the result. Do it for each context, not just the first, because a missing launch placeholder or a typo in one credential only shows up on the context that has it.
for (const ctx of [us, uk]) {
const page = await ctx.newPage();
await page.goto('https://api.ipify.org?format=json');
console.log(await page.textContent('body'));
await page.close();
}
If the address matches your host, the proxy is not applied. If it resolves to a datacenter ASN when you bought mobile, the credential is pointing at the wrong pool. Check what an external observer sees:
Check the exit IP your proxy is presenting
For credentials you have not used yet, run them through the proxy validator first: a Playwright run that fails on a dead credential looks identical to a run blocked by the target site.
Failure modes that look like blocks
- Timeouts on every request. Usually protocol mismatch (
http://written against a SOCKS-only port) or a firewalled port, not detection. Curl the same credential before blaming Playwright. - First page loads, second one hangs. Sticky session expired mid-run and the gateway re-pinned you to a device that the target site already challenged. Set the TTL longer than your longest flow.
- Challenge pages only in headless mode. That is fingerprinting, not the proxy. The proxy fixes the network layer; the browser layer is separate work, and Playwright proxy setup against Cloudflare walks through the full combination with real mobile IPs.
- Works locally, blocked in CI. Your CI region resolves DNS outside the proxy, so lookups leak. Route DNS through the proxy or use hostnames only inside the context.
For a full pipeline where proxy config is one part of a larger scraper, see the Amazon scraping setup.
1How do I set a proxy in Playwright Python?
Pass a proxy dict to launch() or new_context(): {"server": "http://host:port", "username": "...", "password": "..."}. The keys and behavior match the Node API exactly, including the Chromium launch-placeholder requirement for per-context proxies.
2Can I use a different proxy for each page?
Not per page. Proxy scope is the browser or the context, so create one context per proxy and open pages inside it. Pages in the same context share the exit IP and the cookie jar.
3Does Playwright support SOCKS5 proxies?
Yes, with the caveat that Chromium does not support SOCKS5 with username and password authentication. Use HTTP for authenticated credentials, or SOCKS5 with IP whitelisting.
4How do I rotate proxies between Playwright runs?
Rotate the credential, not the code. With a pool endpoint, omit the session parameter for per-request rotation or change the session ID between runs. With a dedicated device, trigger rotation through the API before the next run starts.
5Why does my proxy work in curl but not in Playwright?
Most often the launch option is missing or overridden: a context proxy without a launch-level proxy in Chromium, a config-level use.proxy that a test overrides, or a bypass rule catching your target host. Log the exit IP inside the context to confirm which layer is wrong.
Run Playwright on real 4G/5G devices
Pay-per-GB mobile pools with configurable sticky sessions, or dedicated devices you rotate on demand.