🎯 A customizable, anti-detection cloud browser powered by self-developed Chromium designed for web crawlers and AI Agents.👉Try Now
Back to Blog

How to Use a Proxy With Playwright: The Essential Guide

James Thompson
James Thompson

Scraping and Proxy Management Expert

29-Jun-2026

TL;DR:

  • Playwright drives a proxy from one config option. The proxy parameter 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.
  • proxyCountry pins the exit region; sessionName plus sessionTTL hold 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 Copy
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 Copy
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 Copy
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 parametersproxyCountry, sessionName, and sessionTTL ride 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 Copy
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 Copy
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 Copy
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 Copy
// 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 sessionName and a live sessionTTL, 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.

Most Popular Articles

Catalogue