Documentation Python quickstart Blog Free tools Enterprise solutions hello@quanticdata.ioLog in

Playwright Proxy: Setup, Auth and Bandwidth

Seven pages measured in a browser: the full load moved between 2.0 and 34.5 times the bytes of its own HTML document
Seven pages measured in a browser: the full load moved between 2.0 and 34.5 times the bytes of its own HTML document

Playwright takes a proxy as an object, not as a URL. Pass proxy with server, username and password to launch() for the whole browser, or to newContext() for one context. Credentials written inside the server URL are ignored, which is why most broken setups fail at the first request.

The Playwright proxy object, at launch and per context

There is only one shape to learn. The Playwright docs define the same four fields on browserType.launch() and on browser.newContext(): server, an optional comma-separated bypass list, and an optional username and password pair.

// Node.js — one proxy for the whole browser
const { chromium } = require('playwright');

const browser = await chromium.launch({
  proxy: {
    server: 'http://gw.example.com:8080',
    username: 'sub-user',
    password: 'secret',
  },
});
const page = await browser.newPage();
await page.goto('https://api.ipify.org?format=json');
console.log(await page.textContent('body'));
await browser.close();

Python takes a dictionary with the same keys:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(proxy={
        "server": "http://gw.example.com:8080",
        "username": "sub-user",
        "password": "secret",
    })
    page = browser.new_page()
    page.goto("https://api.ipify.org?format=json")
    print(page.text_content("body"))
    browser.close()

The server value accepts an HTTP, HTTPS or SOCKSv5 endpoint. A bare host:port with no scheme is treated as an HTTP proxy. If the echoed address is the exit rather than your own, the route is live; the proxy tester performs the same check without any code when you want a second opinion on the credentials themselves.

Playwright proxy authentication: the fields, not the URL

The single most common failure is inherited from curl and requests habits: writing the credentials into the endpoint. Playwright parses server as an address, so a user and password embedded in it are silently dropped and the proxy answers with a 407 that surfaces as a blank page. Most teams keep one environment variable and split it at startup:

PROXY_URL=http://sub-user:secret@gw.example.com:8080
function toPlaywrightProxy(raw) {
  const u = new URL(raw);
  return {
    server: u.protocol + '//' + u.host,   // scheme + host + port only
    username: decodeURIComponent(u.username),
    password: decodeURIComponent(u.password),
  };
}

const browser = await chromium.launch({ proxy: toPlaywrightProxy(process.env.PROXY_URL) });

Two details decide whether this works. The decodeURIComponent call matters because gateway passwords often contain reserved characters that were percent-encoded into the URL. And the documented scope of username and password is HTTP and HTTPS proxies: a socks5:// server with credentials is not a supported combination in Chromium, which is why authenticated SOCKS5 endpoints fail where the same credentials work over HTTP. If you need SOCKS specifically, use an endpoint that authorises by IP allowlist instead of by password; both styles are available on the SOCKS5 network. A live 407 from an HTTP endpoint has its own diagnosis path in our 407 Proxy Authentication Required guide.

Per-context proxies: several exits inside one browser

A launch-level proxy applies to every page the browser opens. A context-level proxy applies only to that context, and contexts are cheap, which makes the context the natural unit of rotation:

const browser = await chromium.launch();

for (const job of jobs) {
  const context = await browser.newContext({
    proxy: { server: 'http://gw.example.com:8080', username: job.session, password: 'secret' },
    userAgent: job.userAgent,
  });
  const page = await context.newPage();
  await page.goto(job.url, { waitUntil: 'domcontentloaded' });
  await handle(page);
  await context.close();          // cookies, storage and the exit all go away together
}

Closing the context discards cookies and local storage along with the exit, so the next job starts clean. Two rotation styles then work with this loop. A rotating endpoint hands you a different IP per connection with no code changes at all, which suits page-at-a-time crawling. A sticky session, selected through the username, holds one IP for the length of a multi-step flow such as a search, a filter and a detail page. Choosing between them is a question about the target, not about Playwright; the trade-off is set out in our Playwright scraping guide.

What one page load actually pulls through the exit

Every guide stops at the config object, which hides the part that shows up on the invoice. A proxy meters bytes, and a browser does not fetch a page: it fetches a document and then everything the document asks for. On 11 September 2026 we loaded seven pages once each in a Chromium browser with no extensions and an empty cache for each origin, and read the request count and the wire bytes straight from the Performance Resource Timing API.

PageRequestsDocument on the wireWhole loadMultiple
Hacker News front page65.9 KB11.5 KB2.0x
Playwright docs, Network page1513.2 KB260 KB19.7x
This site, quanticdata.io home2515.5 KB317 KB20.4x
Wikipedia, Web scraping2645.8 KB402 KB8.8x
Stack Overflow question page4856.4 KB718 KB12.7x
BBC News front page7265.8 KB1.24 MB19.4x
GitHub repository page16157.8 KB1.94 MB34.5x

Across the seven pages, 353 requests moved 4.86 MB while the documents themselves accounted for 260 KB. The median page cost 19.4 times its own HTML, and the spread is the interesting part: a plain server-rendered page stayed near parity, an application shell multiplied it by more than thirty. One honest caveat, and it points the same way: resources served cross-origin without a Timing-Allow-Origin header report zero bytes to the timing API, so every figure above is a floor rather than a total.

The bill: metered bandwidth against pay per success

Turn the measurement into money at published rates. Ten thousand loads of the GitHub-sized page move roughly 20.4 GB, which is about $16.30 on residential Basic at $0.80/GB. Fetching only the document for those same ten thousand URLs moves 0.59 GB, about $0.47. The browser, in other words, is not a small surcharge on the proxy bill. It is the bill.

That is the honest case for keeping Playwright where it earns its keep, and for sizing the plan before the first run rather than after it, which is what this sizing walkthrough is for. Where the page does not truly need a browser, the scraping API charges per successful page instead of per gigabyte, from $0.0002 per page and $0.001 with JavaScript rendering, and failures are not billed at all. The same ten thousand rendered pages land at $10 with the browser fleet included.

Trim the page before it costs you

When you keep the browser, stop paying for pixels you never read. Route interception drops whole resource classes before they reach the exit:

await context.route('**/*', route => {
  const type = route.request().resourceType();
  if (type === 'image' || type === 'media' || type === 'font') return route.abort();
  return route.continue();
});

On the pages measured above, images, fonts and media are the bulk of the difference between the document and the whole load. Two cautions. Blocking stylesheets as well will break any selector that depends on layout or visibility, so leave stylesheet alone unless your extraction is purely structural. And an empty image cache is itself a signal on sites that score behaviour rather than headers, which is the boundary described in the stealth write-up: a browser that downloads nothing does not look like a browser.

When the proxy is set and nothing works

Chromium reports proxy failures with numbered network errors, and each number narrows the cause to one layer. The values come straight from the Chromium error list.

ErrorNumberWhat it means
ERR_PROXY_CONNECTION_FAILED-130The proxy host did not resolve, or the socket to it never opened. Wrong host, wrong port, firewall.
ERR_TUNNEL_CONNECTION_FAILED-111You reached the proxy, but its CONNECT to the target failed. Usually credentials, an expired plan, or a blocked destination.
ERR_NO_SUPPORTED_PROXIES-336Nothing in the proxy list is usable by this browser. A scheme it cannot speak, typically an authenticated SOCKS endpoint.
ERR_UNEXPECTED_PROXY_AUTH-323A 407 arrived on a request that was not sent to a proxy at all, so the proxy settings are not applying to that request.
ERR_EMPTY_RESPONSE-324The connection closed with no data. Often the target dropping the exit rather than a proxy fault.

The tunnel failure is the one that swallows the most hours, and it has its own checklist in the ERR_TUNNEL_CONNECTION_FAILED guide. Two further traps have nothing to do with your configuration being wrong:

  • Local addresses skip the proxy. Chromium applies implicit bypass rules to localhost and other loopback addresses, so a request to your own test server never reaches the exit. Subtracting those rules requires the explicit <-loopback> entry in the bypass list, and it is deliberate behaviour, not a Playwright bug.
  • A context proxy beats the launch proxy. If the exit IP does not change when you expect it to, check whether a newContext() call further down the file is setting its own proxy, or whether the target domain matches an entry in bypass. The bypass list is comma separated and a leading dot matches subdomains.

Finally, confirm the exit before blaming the site. Load an IP echo in the same context you are about to scrape with, not in a separate throwaway browser. A proxy that authenticates correctly in one context and not in another is nearly always a configuration that was applied at the wrong level.

Sources & further reading

FAQ

Quick answers on playwright proxy.

Something else? Ask us →

Does Playwright support SOCKS5 proxies?

Yes. Set the server to a socks5 endpoint, for example socks5://gw.example.com:1080. The catch is authentication: the documented username and password fields apply to HTTP and HTTPS proxies, so an authenticated SOCKS5 endpoint typically fails with ERR_NO_SUPPORTED_PROXIES. Use an IP-allowlisted SOCKS endpoint, or switch that job to the HTTP port.

Can I change the proxy without restarting the browser?

Yes, by creating a new browser context. The proxy passed to launch() is fixed for the life of the browser, but every newContext() call can carry its own proxy object. Closing the context releases the exit along with its cookies and storage, which is the cleanest way to rotate between jobs.

Why does my Playwright proxy not apply to localhost?

Chromium has implicit proxy bypass rules for loopback addresses, so requests to localhost or 127.0.0.1 go direct no matter what the proxy setting says. Add the explicit <-loopback> entry to the bypass list if you genuinely need local traffic to traverse the proxy, which is normally only the case when you are recording it.

How do I rotate proxies in Playwright?

Two ways, and they combine. Point every context at a rotating endpoint and let the gateway pick a fresh IP per connection, or create one context per job with a session identifier in the username to hold an IP for a multi-step flow. Restarting the whole browser per proxy works too, but pays a process launch you do not need.

How much bandwidth does one Playwright page load use?

Far more than the HTML. Across seven pages measured on 11 September 2026, the median load moved 19.4 times the bytes of its own document, ranging from 2x on a plain server-rendered page to 34.5x on an application shell. Budget from the whole-page figure, never from the size of the HTML you are parsing.

Should I use a proxy in Playwright or a scraping API?

Use Playwright when the data only exists after interaction: a login, a filter, a click-through. Use a pay-per-success API when you need the rendered page and nothing more, because it charges per successful page rather than per gigabyte and absorbs the browser fleet, the retries and the blocked attempts.

Stop paying per gigabyte for pages you only read once

Keep Playwright for the flows that need a real browser, and let the API return the rest as clean Markdown or JSON, billed per successful page. Every account gets $2 of free API usage each month.

Related reading