How to Use a Proxy With Playwright: The Essential Guide
Scraping and Proxy Management Expert
TL;DR:
- Playwright drives a proxy from one config option. The
proxyparameter takes a server, username, and password, and the same shape works whether you launch a local browser or connect to a remote one. - A residential exit is what makes headless traffic read as human. Datacenter IP ranges and the default automation fingerprint are the two signals public sites check first; routing through real residential addresses removes the first.
- The Scrapeless Scraping Browser speaks Chrome DevTools Protocol. Playwright attaches to it with
chromium.connectOverCDP(endpoint), so residential proxies and anti-detection run cloud-side while your script stays plain Playwright. proxyCountrypins the exit region;sessionNameplussessionTTLhold one IP across navigations. Geo-targeting and sticky sessions are query parameters on the connection string, not extra application code.- Parallel runs scale through independent sessions. Open several connections at once and each gets its own residential IP, with no concurrent-seat ceiling.
- Free to start. New Scrapeless accounts include free Scraping Browser runtime — sign up at app.scrapeless.com.
Introduction: Playwright proxies, and where raw config stops
Playwright drives Chromium, Firefox, and WebKit from a single API, and its auto-waiting makes navigations less brittle than the older generation of automation tools. Two things still get a Playwright script blocked. The browser ships an automation fingerprint that detection scripts read, and it connects from whatever IP the host machine has — usually a cloud or datacenter address that public sites rate-limit or geo-restrict. A residential proxy answers the network half of that problem; an anti-detection browser answers the fingerprint half.
This guide starts with Playwright's own proxy support, since that is the option every proxy plugs into. It then shows where raw proxy configuration runs out of road, and how to hand the harder cases to a cloud browser that Playwright connects to over the same protocol it already uses for Chromium.
Prerequisites
- Node.js 18 or newer
- A Scrapeless account and API key — sign up at app.scrapeless.com
- Playwright installed (
npm install playwright) - Basic familiarity with npm and Node.js environment variables
Install
1. Add Playwright to your project
If you don't have Playwright yet, install it:
bash
npm install playwright
This pulls in the Playwright library for all three browser engines.
2. Set your Scrapeless API key
Store the key as an environment variable so it never lands in source control:
bash
export SCRAPELESS_API_KEY="your_api_key_here"
3. No wrapper SDK required
Playwright connects to the Scrapeless Scraping Browser over the Chrome DevTools Protocol, so there is no extra package to install beyond Playwright itself.
Configure Playwright's built-in proxy
Playwright accepts proxy settings in launch() and in newContext(). Point server at your proxy gateway and pass the credentials:
javascript
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({
proxy: {
server: 'http://your-proxy-host:8080', // your proxy gateway
username: 'your-proxy-user',
password: 'your-proxy-pass',
},
});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();
})();
That is the whole library-level surface, and it works the same across Chromium, Firefox, and WebKit. Two things it does not do: it assumes you already have a pool of residential addresses to point server at, and it leaves the browser's automation fingerprint untouched. Raw proxy config moves your traffic to a new IP; it does not change how the page reads the client.
Get your API key on the free plan: app.scrapeless.com
Where raw proxy config stops — fingerprints and IP sourcing
Two gaps sit between launch({ proxy }) and an automation run that public sites serve cleanly: sourcing and rotating a real residential pool, and the fingerprint the browser presents once it arrives. The Scrapeless Scraping Browser closes both. It is an anti-detection cloud browser built for web crawlers and AI agents, and because it speaks Chrome DevTools Protocol, Playwright attaches to it with one line. For Playwright specifically, it brings:
- Residential exit nodes in 195+ countries, with the automation fingerprint handled cloud-side.
- Cloud-side JavaScript rendering — the remote browser executes the page, so the work leaves your machine.
- Geo-targeting and sticky sessions as connection parameters —
proxyCountry,sessionName, andsessionTTLride on the endpoint string. - No concurrent-seat limit — pay per use and open as many sessions as the run needs.
The connection string carries your key as the token parameter; the docs at docs.scrapeless.com list every option. Get your API key on the free plan at app.scrapeless.com.
Connect Playwright over CDP
Build the WebSocket endpoint, then connect with chromium.connectOverCDP(). From there it is ordinary Playwright — the proxy and fingerprinting are already applied on the remote browser:
javascript
const { chromium } = require('playwright');
const token = process.env.SCRAPELESS_API_KEY;
const params = new URLSearchParams({
token,
proxyCountry: 'US', // pin the residential exit region
sessionTTL: '300', // keep the session alive for 300 seconds
});
const wsEndpoint = `wss://browser.scrapeless.com/api/v2/browser?${params.toString()}`;
(async () => {
const browser = await chromium.connectOverCDP(wsEndpoint);
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log('Title:', await page.title());
// Confirm the egress is a residential IP, not the host machine's
const ipPage = await context.newPage();
await ipPage.goto('https://api.ipify.org?format=json', { waitUntil: 'domcontentloaded' });
console.log('Egress:', await ipPage.evaluate(() => document.body.innerText));
await browser.close();
})();
Pin a country and reuse one IP
Geo-targeting is the proxyCountry parameter. Stickiness is sessionName plus a live sessionTTL: reuse the same name within the window and every navigation leaves from the same residential IP, which is what keeps a logged-in flow on one identity.
javascript
const { chromium } = require('playwright');
const token = process.env.SCRAPELESS_API_KEY;
function endpoint({ country, session }) {
const params = new URLSearchParams({
token,
proxyCountry: country,
sessionName: session, // same name + live TTL = same exit IP
sessionTTL: '300',
});
return `wss://browser.scrapeless.com/api/v2/browser?${params.toString()}`;
}
(async () => {
const browser = await chromium.connectOverCDP(endpoint({ country: 'US', session: 'demo-session' }));
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://api.ipify.org?format=json', { waitUntil: 'domcontentloaded' });
console.log('First :', await page.evaluate(() => document.body.innerText));
await page.goto('https://api.ipify.org?format=json', { waitUntil: 'domcontentloaded' });
console.log('Second:', await page.evaluate(() => document.body.innerText));
await browser.close();
})();
Run sessions in parallel
Each connection is independent, so a worker pool is Promise.all over several endpoints with distinct session names. Keep concurrency to three per host to stay polite:
javascript
const { chromium } = require('playwright');
const token = process.env.SCRAPELESS_API_KEY;
function endpoint(session) {
const params = new URLSearchParams({ token, proxyCountry: 'US', sessionName: session, sessionTTL: '180' });
return `wss://browser.scrapeless.com/api/v2/browser?${params.toString()}`;
}
async function fetchIp(session) {
const browser = await chromium.connectOverCDP(endpoint(session));
try {
const page = await browser.newContext().then(c => c.newPage());
await page.goto('https://api.ipify.org?format=json', { waitUntil: 'domcontentloaded' });
return { session, ip: JSON.parse(await page.evaluate(() => document.body.innerText)).ip };
} finally {
await browser.close();
}
}
(async () => {
const workers = ['worker-1', 'worker-2', 'worker-3'];
const results = await Promise.all(workers.map(fetchIp));
console.log(results);
})();
What You Get Back
Each session reports a distinct residential exit, and a sticky session reports the same one twice. The parallel run above returns one record per worker:
javascript
// Illustrative sample — schema is exact; the IP value is illustrative and varies per session.
[
{ "session": "worker-1", "ip": "72.188.194.89" },
{ "session": "worker-2", "ip": "172.56.218.152" },
{ "session": "worker-3", "ip": "73.135.52.138" }
]
Schema notes:
proxyCountry: 'US'pins each exit to a US residential address; change the code to target another region.- Within one
sessionNameand a livesessionTTL, the IP stays constant across navigations; a new name draws a fresh address. - Selectors must match the target site's current markup — tighten them when the DOM shifts.
- The remote browser renders JavaScript by default, so client-side content is present before you read the DOM.
Troubleshooting
The connection drops part-way through a long run
Cause: sessionTTL expired before the workflow finished.
Fix: Set sessionTTL to cover the whole flow up front — it is the session's lifetime in seconds, so size it to the longest navigation chain you expect.
The site still serves an "Access Denied" page
Cause: A clean residential IP is not the only signal; a cold session that jumps straight to a deep URL still looks automated.
Fix: Warm the session by loading the site's homepage first in the same context, pin proxyCountry to a region that matches the audience, and give the context realistic options (userAgent, Accept-Language).
Extraction returns empty fields
Cause: The selectors no longer match the rendered markup.
Fix: Re-inspect the page and tighten the selectors; guard optional fields so a missing element returns null instead of throwing.
Conclusion: a proxy layer that stays out of your application code
A proxied Playwright run reduces to a few moves: build the endpoint, connect over CDP, pin the country, and write the same Playwright you already know. Native launch({ proxy }) covers the case where you own the pool; the Scraping Browser covers the cases where you need real residential exits and a fingerprint that holds up. Pin the exit with proxyCountry, keep one identity with sessionName plus a live sessionTTL, hold concurrency at three per host, and treat absent fields as nullable. For a wider look at rotation strategies, see best-rotating-proxies-2026.
Ready to Automate at Scale With Playwright and Proxies?
Join our community to claim a free plan and connect with developers building Playwright automation: Discord · Telegram.
Sign up at app.scrapeless.com for free Scraping Browser runtime, review pricing, and adapt the patterns above to the regions and sites your automation needs.
FAQ
Q: Is scraping with Playwright legal?
Scraping publicly visible data is broadly permitted, but the rules vary by jurisdiction and by a site's terms of service. Review the target's terms, avoid data behind a login you are not authorized to use, and consult counsel for anything sensitive.
Q: Do I need a proxy?
You need one whenever the target blocks datacenter IPs or applies geo-restrictions, which covers most public sites at scale. A residential exit lets the request arrive as ordinary user traffic; without it, a cloud IP is an easy block.
Q: I get an "Access Denied" page — how do I get a clean render?
Load the site's homepage first in the same context to warm the session, pin proxyCountry to a region that fits the audience, and set realistic context options like userAgent and Accept-Language. A deep URL hit from a cold context is the usual trigger.
Q: Can I run many Playwright sessions at once?
Yes. Each CDP connection is independent and draws its own residential IP, so a worker pool is Promise.all over several endpoints with distinct sessionName values. Keep concurrency to three per host so you stay within polite limits.
Q: How do I switch the exit country?
Change the proxyCountry parameter and open a new connection. The country is fixed when the session is created, so a different region means a new endpoint string rather than a mid-session change.
Q: Does this work on JavaScript-heavy sites?
Yes. The remote browser executes the page before you read it, so client-rendered content is in the DOM by the time page.goto() resolves. Use waitUntil: 'domcontentloaded' plus a short settle on sites whose network never goes idle.
Q: What happens when the site's markup changes?
Selectors tied to a specific class or structure break when a site ships a redesign. Anchor extraction on the most stable signal available — a data attribute or a durable URL pattern over a hashed CSS class — and re-check the selectors when fields start coming back empty.
At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.



