Using a proxy with Playwright and Puppeteer
Published: 23 July 2026 · 6 min read
TL;DR: In Playwright, pass a proxy dict to launch() or, better, to new_context() so each context has its own IP. In Puppeteer, pass --proxy-server in args and call page.authenticate() for credentials. Use a residential proxy so the browser exits from a real home IP, and a separate sticky session per context/browser when you run several identities at once.
Playwright
Playwright takes a proxy dictionary with a bare server URL and separate credential fields. The simplest form sets it on launch(), which applies to the whole browser:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(
proxy={
"server": "http://gw.roamproxy.com:41080",
"username": "YOUR_USERNAME",
"password": "YOUR_PASSWORD",
}
)
page = browser.new_page()
page.goto("https://ip.sb")
print(page.inner_text("body"))
browser.close()
Per-context proxy - one IP per identity
The more useful pattern sets the proxy on new_context() instead. Each context is an isolated browser session - its own cookies, storage and now its own IP - so a single browser process can run several identities that do not share an exit:
ctx_a = browser.new_context(proxy={
"server": "http://gw.roamproxy.com:41080",
"username": "USERNAME_session_a",
"password": "YOUR_PASSWORD",
})
ctx_b = browser.new_context(proxy={
"server": "http://gw.roamproxy.com:41080",
"username": "USERNAME_session_b",
"password": "YOUR_PASSWORD",
})
Give each context a different sticky session in the username and each gets a stable, distinct residential IP - see what are rotating proxies for how the session string works.
Puppeteer
Puppeteer takes the proxy as a Chromium launch flag and handles authentication on the page. The credentials do not go in the flag:
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
args: ['--proxy-server=http://gw.roamproxy.com:41080'],
});
const page = await browser.newPage();
await page.authenticate({ username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD' });
await page.goto('https://ip.sb');
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
Puppeteer applies the proxy to the whole browser, so to run several IPs at once you launch several browsers (each with its own --proxy-server and sticky session), or put a proxy router in front.
Verify the exit
Both snippets above navigate to an IP-check endpoint and print the result. It should show the Roam residential exit IP and the country you selected, not your machine's real address. If it shows your real IP, the proxy did not attach - re-check the FAQ.
When you don't need a browser
Playwright and Puppeteer are the right tool when a target requires JavaScript or an interactive challenge. When it does not - a plain JSON API or static HTML - a full browser is slower and heavier than you need. For scripted HTTP requests with a browser-accurate fingerprint, use curl_cffi instead, and see how to get past Cloudflare for choosing between the two by defense layer. To drive Playwright with an LLM, see browser-use with residential proxies.
FAQ
Should I set the proxy on launch or on the context?
Prefer the context in Playwright. Setting it on launch() applies one proxy to the whole browser; setting it on new_context() lets each context have its own proxy, which is what you want when one script runs several identities - each context becomes a separate browser session with a separate IP. Puppeteer does not have first-class per-context proxies, so for multiple IPs you either launch multiple browsers or use a proxy-router in front.
How do I pass the username and password?
In Playwright, put them in the proxy dict as username and password alongside server - never inside the server URL. In Puppeteer, the credentials do not go in args; you set --proxy-server=host:port in args and then call await page.authenticate({username, password}) before the first navigation.
Do I still need curl_cffi if I'm already using a real browser?
No - that is the point of using a browser. Playwright and Puppeteer drive real Chromium, so the TLS/JS fingerprint is genuine already. curl_cffi exists to give that same fingerprint to scripted HTTP requests that are not using a browser. With Playwright/Puppeteer you only need to fix the network layer, which is what the proxy does.
The browser launches but still shows my real IP - why?
Usually the proxy was set on the wrong object or the credentials were embedded in the server URL. Confirm the server value is a bare http://host:port, that username/password are separate keys (Playwright) or set via page.authenticate() (Puppeteer), and that you attached the proxy to the context/browser you actually navigate with. Then re-run the IP check below.
Both frameworks expect a plain host:port proxy plus credentials - exactly what Roam gives you. Rotating residential at $2/GB and static residential at $4/IP per month, over HTTP and SOCKS5. Create an account and get 300MB of free trial traffic to point your first browser at a residential exit.