Playwright proxy setup

How to use proxies in Playwright

Where the proxy goes in launch and context options, how auth actually works, and a debug order that separates proxy failures from target blocks.

Field notes Setup checks Updated 2026-06-12

Create the page inside the proxied context

Playwright accepts a proxy at browser launch or on an individual browser context, and most reports of Playwright ignoring the proxy come down to which of those owns the page. A page opened with browser.newPage() belongs to the default context, so a browser.newContext({ proxy }) created afterwards never covers it. The fix is to create the proxied context first, then open the page from that context.

import { chromium } from 'playwright';
                        
                        const browser = await chromium.launch();
                        
                        const context = await browser.newContext({
                          proxy: {
                            server: 'http://proxynade.net:2555',
                            username: process.env.PROXY_USER,
                            password: process.env.PROXY_PASS
                          }
                        });
                        
                        const page = await context.newPage();
                        await page.goto('https://example.com');

Playwright wants the host in server and the credentials in the separate username and password fields (see Playwright proxy docs), so resist pasting a full http://user:pass@host:port URL into server. Setting credentials through setExtraHTTPHeaders() does not work either, because headers authenticate you to websites, not to the proxy tunnel.

On a Proxynade pool plan the username carries the routing options, so PROXY_USER is the expanded form, for example rt97db6958d9-plan-volume-country-us-lifetime-30: the base username, a required plan token (volume, premium, or datacenter), an optional lowercase country code, and an optional rotation lifetime in minutes. Datacenter takes a lifetime token only on a sticky session, and the password stays exactly as issued.

Fix page.goto: net::ERR_NO_SUPPORTED_PROXIES

page.goto: net::ERR_NO_SUPPORTED_PROXIES usually means Chromium rejected the proxy scheme or the shape of the proxy object before any request left the machine, which also means it is not a target-site block. It is worth checking the proxy line itself before touching the scraper logic.

SymptomLikely causeFix
ERR_NO_SUPPORTED_PROXIESUnsupported or malformed proxy URLUse http://host:port or socks5://host:port in server.
407 Proxy Authentication RequiredProxy saw the browser, but auth failedSplit username and password into fields. URL-encode special characters.
Target returns 403Proxy worked, target refused the requestChange target strategy. Do not debug this as proxy auth.
Your real IP appearsPage came from the wrong contextSearch for browser.newPage() and replace it with context.newPage().

One Chromium quirk worth knowing: it will route through a socks5://host:port proxy, but it never sends a username and password to a SOCKS proxy. Since the Proxynade pool gateway requires credentials, browser jobs should use the http://proxynade.net:2555 form with the username and password fields.

Verify with logs, not public IP echo pages

Public IP echo sites get hammered by proxy users, so they cache, rate-limit, and sometimes lie by accident. When you can, point the test at an endpoint you control, log the source IP, user agent, request time, and status, and compare that record with the proxy dashboard. For sticky login flows, keep every page in the same BrowserContext, because a new context presents a new browser identity and the site will see a different session shape.

Keep in mind that your script byte counter is not the billing meter. The proxy also sees CONNECT setup, redirects, blocked HTML, images, fonts, failed retries, and requests that die before Playwright gives your code a clean response, so the two numbers will never match exactly.

Bandwidth cuts that actually matter

If the target works without images, fonts, or media, block them at the context level. The savings are largest when a retry loop keeps reloading the same document after a soft block, because each wasted reload then costs a fraction of what it did.

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

Hold off on resource blocking until the proxy itself is verified, since aborting requests changes how the page loads and can shift the browser fingerprint. When you measure the effect, measure kept rows per gigabyte rather than bandwidth alone.

Browser launch proxy vs context proxy

Both proxy locations are legitimate. A launch-level proxy suits a browser that should use one route for everything, while context-level proxies let one browser process hold several isolated identities, which is the usual shape for scraping workers.

Teams usually trip on the mix: one default page for setup, then a proxied context for the real work. Cookies, service workers, and open sockets from that first page can still shape the run, so a clean proxy test starts from a fresh context and a page created inside it.

Proxy locationUse it whenFailure pattern
Launch proxyOne browser, one routeHarder to mix identities by mistake.
Context proxyMany isolated routes in one browserEasy to leak with browser.newPage().
No proxy on setup pageNever for proxy verificationThe setup page proves the wrong network path.

Rotating proxies across parallel workers

Context proxies are how Playwright rotates without restarting the browser: one Chromium process, one context per proxy assignment, one page per context. Each worker takes its own proxy line, runs its task, and closes the context, while any live page keeps the tunnel it started with.

for (const line of proxyLines) {
                          const context = await browser.newContext({
                            proxy: {
                              server: 'http://proxynade.net:2555',
                              username: line.user,
                              password: line.pass
                            }
                          });

                          try {
                            const page = await context.newPage();
                            await page.goto(target, { waitUntil: 'domcontentloaded' });
                            // collect, then move on
                          } finally {
                            await context.close();
                          }
                        }

Two boundaries still matter. A new context gives you a new exit only when the assignment behind it is new, so on a Proxynade pool either use a fresh expanded username per task or set the rotation window with the lifetime-<minutes> token. And when a sticky flow needs the same exit for half an hour, keep that one context alive instead of recreating it per page.

Logging shape for Playwright proxy tests

Success or failure alone is not enough to debug a proxy run. Record the context label, proxy label, target host, status code, final URL, retry number, and a count of blocked resources, because a target block and a proxy auth failure only look different when the log carries those fields.

{
                          "tool": "playwright",
                          "proxy_label": "us-res-01",
                          "context_id": "checkout-flow-7",
                          "target_host": "example.com",
                          "status": 403,
                          "final_url": "https://example.com/blocked",
                          "retry": 1,
                          "images_blocked": 42
                        }

The code sample at the top of this page starts the run; this log shape is what explains the run when it fails in production.

Playwright proxy FAQ

Can Playwright use SOCKS5 proxies? Yes, with a socks5://host:port server value, but Chromium does not send credentials to SOCKS proxies. When the gateway needs a username and password, use the http form with the username and password fields.

Why does Playwright show my real IP? The usual reason is page creation: a page opened with browser.newPage() is not covered by a later proxied context.

Does Playwright proxy auth use headers? No. Credentials go in the proxy username and password fields, because headers authenticate to websites, not to the proxy tunnel.

Should I launch one browser per proxy? Use one browser per hard rotation and one context per sticky identity. A live page cannot swap tunnels cleanly.

Does an http:// proxy work for HTTPS sites in Playwright? Yes. The context opens a CONNECT tunnel through the proxy and runs TLS inside it, so one http://host:port line covers both http and https targets.

How do I rotate proxies in Playwright? One context per proxy assignment, one page per context, close the context when the task ends. A new context only means a new exit when the assignment behind it is new.

A real debug order

  1. Run a one-request curl test with the same credentials.
  2. Open a Playwright context with the proxy set.
  3. Create the page from that context.
  4. Navigate to your own logging endpoint.
  5. Check the source IP in the server log.
  6. Only then load the real target.

This order keeps target blocking out of the proxy-auth check, and it surfaces stale environment variables and pasted whitespace before they cost an evening.