How to Use a Proxy With Playwright (Node & Python)
To route Playwright through a proxy, pass a proxy option when you launch the browser (or when you create a browser context). The option takes a server URL plus optional username and password, and it works the same way in both the Node and Python bindings. The key decision is where you set it: at launch, every page uses the same proxy settings; per context, each context gets its own settings (for example, its own country) and its own isolated cookies. The examples use HOST:PORT, USERNAME and PASSWORD as placeholders — the real values are on your dashboard.
The fastest working example (Node)
Here is the minimum that routes a headless Chromium session through an authenticated proxy and prints the egress IP it actually used:
// save as ip.mjs and run: node ip.mjs
import { chromium } from "playwright";
const browser = await chromium.launch({
proxy: {
server: "http://HOST:PORT",
username: process.env.PROXY_USER, // USERNAME
password: process.env.PROXY_PASS, // PASSWORD
},
});
const page = await browser.newPage();
await page.goto("https://api.ipify.org?format=json");
console.log(await page.textContent("body"));
await browser.close();If the printed IP is the proxy's and not your own, traffic is flowing through it. Always verify with an IP-echo endpoint before you trust a run — a context created without the proxy option goes out directly, and nothing warns you.
The same thing in Python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(proxy={
"server": "http://HOST:PORT",
"username": "USERNAME",
"password": "PASSWORD",
})
page = browser.new_page()
page.goto("https://api.ipify.org?format=json")
print(page.text_content("body"))
browser.close()Launch-level vs per-context proxies
Setting proxy on launch() applies it to the whole browser. Setting it on browser.newContext() scopes it to one context — and a context is an isolated cookie jar, cache, and storage partition. This is the pattern that matters for scraping:
const browser = await chromium.launch();
const ctxA = await browser.newContext({
proxy: { server: "http://HOST:PORT",
username: "USERNAME-country-us", password: "PASSWORD" },
});
const ctxB = await browser.newContext({
proxy: { server: "http://HOST:PORT",
username: "USERNAME-country-de", password: "PASSWORD" },
});Each context now exits from a different country, with no shared cookies between them — so a block on one doesn't taint the other. The country suffix is a lowercase ISO code. The username can also carry -rotate, but leave it off for a browser: it gives every new connection a different exit, which scatters one page's sub-requests (more below). One caveat specific to o2co: the gateway chooses the exit per source machine, not per context, so two contexts asking for the same country from one machine can share an exit. For a multi-step flow (login, navigate, paginate) that must hold one IP, ask for sticky mode on your account; in the default mode an active flow keeps its exit until the machine goes idle.
SOCKS5 or HTTP? Use the HTTP form
Playwright accepts a socks5:// server just as readily as http://, and with SOCKS5 Chromium resolves DNS remotely through the proxy. The catch: Chromium does not support username/password auth on a SOCKS5 proxy via the launch option. So for an authenticated endpoint, use the http:// form with the username/password fields — exactly what the examples above do. There is no DNS downside: the browser hands the hostname to the proxy inside the CONNECT request, so resolution still happens at the proxy. o2co's endpoint speaks HTTP and SOCKS5 on the same port, so the HTTP form reaches the same pool. If you need a primer on the protocol itself, see what a SOCKS5 proxy is and SOCKS5 vs HTTP proxy.
Handling proxy authentication prompts
The username/password fields in the proxy option are the right way to authenticate — Playwright answers the proxy's 407 challenge for you. Avoid embedding credentials directly in the server URL (http://user:pass@host); browsers are inconsistent about parsing that, and it leaks the secret into logs. Keep credentials in the dedicated fields, sourced from environment variables, not hard-coded.
Rotation strategy that doesn't get you blocked
It's tempting to rotate the IP on every request, but for a browser that backfires. A single page load fires dozens of sub-requests for scripts, images, and XHR within a second or two; if those land on different IPs the target sees an impossible session and trips its anti-bot. Treat the context as the unit of identity instead — one identity per context, a fresh context per logical task. With the plain username our gateway keeps a machine's burst of connections on one exit, so a page's sub-requests don't scatter; the -rotate suffix is for crawlers fetching independent pages, not for browsers — and since a browser reuses its connections, rotation (which is per connection) would not mean a new IP per request anyway. We unpack this in what is a rotating proxy.
Don't forget the rest of the fingerprint
A proxy changes your IP, not your browser. Modern detection layers also read your TLS and browser fingerprint, navigator properties, and behavioral signals — a headless browser with a residential IP but an obvious automation fingerprint still stands out. Match the IP type to the target: residential IPs for consumer sites that scrutinize datacenter ranges, datacenter for tolerant or API-style targets. See residential vs datacenter proxies and how websites detect proxies for what actually moves the needle.
Responsible use
Automating a browser through a proxy is a normal part of QA, monitoring, and legitimate data collection — but respect each target's terms of service and robots.txt, throttle your concurrency, and never collect personal data you have no right to. Slower, well-behaved crawls are also more reliable: they look human, and they don't burn the IP reputation you're paying for. For the broader picture, see proxies for web scraping; driving Selenium instead? See Selenium with a proxy. Browser pages are heavy and you pay per GB, so block images and fonts you don't need with page.route(). Get 1 GB free to try it, or see pricing.