Create the page inside the proxied context
Start here before blaming the proxy pool. In Playwright, a proxy can live on the browser launch or on a browser context. If your code calls browser.newPage(), that page comes from the default context. A later browser.newContext({ proxy }) will not cover the page you already opened.
The fix is boring. Create the context first. Then create 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');
Do not paste http://user:pass@host:port into server. Playwright expects the host in server and credentials in username and password. Also, setExtraHTTPHeaders() is not proxy authentication.
Fix page.goto: net::ERR_NO_SUPPORTED_PROXIES
page.goto: net::ERR_NO_SUPPORTED_PROXIES usually means Chromium rejected the proxy scheme or the proxy object shape. It is not a target-site block. Check the line before changing your scraper.
| Symptom | Likely cause | Fix |
|---|---|---|
ERR_NO_SUPPORTED_PROXIES | Unsupported or malformed proxy URL | Use http://host:port or socks5://host:port in server. |
407 Proxy Authentication Required | Proxy saw the browser, but auth failed | Split username and password into fields. URL-encode special characters. |
Target returns 403 | Proxy worked, target refused the request | Change target strategy. Do not debug this as proxy auth. |
| Your real IP appears | Page came from the wrong context | Search for browser.newPage() and replace it with context.newPage(). |
Verify with logs, not public IP echo pages
Public IP check sites get hammered by proxy users. They cache, block, rate-limit, and lie by accident. Use your own endpoint when possible. Log the source IP, user agent, request time, and status. Then compare that with the proxy dashboard.
For sticky login flows, keep every page in the same BrowserContext. A new context is a new browser identity. If you mix contexts, the site sees a different session shape.
Your script byte counter is not the billing meter. The proxy sees CONNECT setup, redirects, blocked HTML, images, fonts, failed retries, and requests that die before Playwright gives your code a clean response.
Bandwidth cuts that actually matter
If the target does not need images or font files, block them at the context level. This is especially useful when a retry loop keeps reloading the same document after a soft block.
await context.route('**/*', route => {
const type = route.request().resourceType();
if (['image', 'font', 'media'].includes(type)) {
return route.abort();
}
return route.continue();
});
Do this after the proxy works. Blocking resources can change browser fingerprints. Measure the kept rows, not only cheaper bandwidth.
Browser launch proxy vs context proxy
Playwright supports a proxy at browser launch and at context creation. Use launch-level proxy when the whole browser should use one route. Use context-level proxy when the same browser process needs separate identities.
The context choice is where teams trip. They create one default page for setup, then create a proxied context for the real work. Cookies, service workers, and open sockets from that first page can still shape the run. A clean proxy test should start from a fresh context and a page created inside it.
| Proxy location | Use it when | Failure pattern |
|---|---|---|
| Launch proxy | One browser, one route | Harder to mix identities by mistake. |
| Context proxy | Many isolated routes in one browser | Easy to leak with browser.newPage(). |
| No proxy on setup page | Never for proxy verification | The setup page proves the wrong network path. |
Logging shape for Playwright proxy tests
Do not log only success or failure. Log the context label, proxy label, target host, status code, final URL, retry number, and resource type count. A target block and a proxy auth failure look different when the log has 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
}
This is the part that makes the page useful in production. The code sample starts the run. The log shape explains why the run failed.
Playwright proxy FAQ
Can Playwright use SOCKS5 proxies? Yes. Use a socks5://host:port server value when the proxy and browser support it. Keep the page inside the context that owns the proxy.
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. Use the proxy username and password fields. Headers authenticate to websites, not to the proxy tunnel.
Should I launch one browser per proxy? Use one browser per hard rotation. Use one context per sticky identity. Do not expect a live page to swap tunnels cleanly.
A real debug order
- Run a one-request curl test with the same credentials.
- Open a Playwright context with the proxy set.
- Create the page from that context.
- Navigate to your own logging endpoint.
- Check the source IP in the server log.
- Only then load the real target.
This order keeps target blocking out of the proxy-auth check. It also catches old environment variables and pasted whitespace early.