Use --proxy-server for the proxy host
Puppeteer will not pick up a proxy just because a URL sits somewhere in your config. Chromium needs the proxy host as a launch argument, and the credentials travel separately: the host and port go into --proxy-server, while the username and password go to page.authenticate() before the first navigation.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
args: ['--proxy-server=http://proxynade.net:2555']
});
const page = await browser.newPage();
await page.authenticate({
username: process.env.PROXY_USER,
password: process.env.PROXY_PASS
});
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
Avoid embedding user:pass@ in the launch argument. Chromium ignores credentials placed there, and the string still leaks into process lists and shell history.
On a Proxynade pool, PROXY_USER is the expanded username that carries the routing options, 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. On datacenter, the lifetime token applies only to a sticky session, and the password goes in unchanged.
Separate 407 from target blocking
A 407 Proxy Authentication Required comes from the proxy itself, while a 403, a captcha, or an empty search result comes from the target. Keeping those two sources separate saves more debugging time than any other habit on this page.
| Result | What it means | Next check |
|---|---|---|
407 | Credentials failed at the proxy | Check username, password, account balance, and special characters. |
| Navigation timeout | Target, network, or proxy route stalled | Test a small HTML endpoint before loading a heavy page. |
403 | The proxy connected, but target refused | Change headers, pacing, session age, or target policy. |
| Same IP every request | Browser reused the same tunnel | Restart the browser or create a new proxy assignment. |
HTTP proxies, HTTPS targets, and the SOCKS5 trap
An http://host:port proxy line carries HTTPS targets fine: Chromium opens a CONNECT tunnel through the proxy and runs TLS to the site inside it, so there is no separate "https proxy" to buy and nothing to change for https:// URLs. The scheme in --proxy-server describes how the browser talks to the proxy, not which sites it can load through it.
SOCKS5 is the one to watch in Puppeteer. Chromium routes through socks5://host:port, but it never sends credentials to a SOCKS proxy, and page.authenticate() does not change that. On an authenticated gateway like a Proxynade pool line, use the http://proxynade.net:2555 form and keep the credentials in page.authenticate().
| Error | What Chromium is saying | Fix |
|---|---|---|
ERR_PROXY_CONNECTION_FAILED | Could not reach the proxy host at all | Check host, port, and firewall, and confirm the launch arg actually made it into args. |
ERR_TUNNEL_CONNECTION_FAILED | Proxy refused the CONNECT to the target | Usually auth or balance on the proxy side. Run the health check below before blaming the target. |
ERR_NO_SUPPORTED_PROXIES | Malformed or unsupported proxy URL | Use http://host:port or socks5://host:port and nothing else in the string. |
ERR_SOCKS_CONNECTION_FAILED | SOCKS handshake failed | If the gateway needs credentials, switch to the http form: Chromium will not auth to SOCKS. |
Rotation needs process boundaries
Swapping the proxy string in memory does not guarantee a new exit, because Chromium keeps connections alive and reuses them. For strict per-task rotation, start a fresh browser with a fresh proxy assignment, and for sticky sessions keep the same browser open until the account flow is done. On a Proxynade pool the sticky side lives in the username: the lifetime-<minutes> token sets the rotation window, so a login flow that needs half an hour on one exit would use lifetime-30.
If the goal is only lower bandwidth rather than a new exit, block the obvious waste once the page loads correctly.
await page.setRequestInterception(true);
page.on('request', request => {
const type = request.resourceType();
if (['image', 'font', 'media'].includes(type)) {
return request.abort();
}
return request.continue();
});
One side effect worth testing: page.authenticate() turns on request interception internally, which can affect throughput (see Puppeteer: network interception). Benchmark with the same auth path you run in production rather than an unauthenticated shortcut.
The proxy dashboard sees more than Puppeteer
Puppeteer reports what your page code observes, while the proxy meter sees the lower-level traffic underneath it: redirect chains, failed TLS attempts, blocked HTML, font files, and retries all count. That gap is why app-level counters usually look cleaner than the bandwidth bill.
A minimal Puppeteer proxy health check
Before running the real target, run one small navigation through the same browser shape you will use for the real work. A health check made with a different HTTP library only proves the credentials, not Puppeteer's launch wiring.
async function proxyHealthCheck(page) {
const started = Date.now();
const response = await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30000
});
return {
status: response?.status(),
finalUrl: page.url(),
ms: Date.now() - started
};
}
A 407 here means credentials. A timeout points at the route or the target, so try a smaller page next. A target block means Puppeteer reached the site, and the proxy setup is no longer the main question.
When to restart the browser
Restart for hard proxy rotation, major credential changes, and any test where you must prove a new exit, and keep the browser alive for the length of one sticky account flow. Mixing those two goals causes the classic bug where the code says it rotated while Chromium quietly kept using an old connection.
For queue workers, the cleanest model treats the browser as part of the proxy assignment: a task gets a proxy label, a browser instance, and a retry budget, and the browser closes when the task ends. That costs some startup time, but it makes rotation honest.
Puppeteer proxy FAQ
Where do proxy credentials go in Puppeteer? Put the proxy host in --proxy-server and the credentials in page.authenticate() before the first navigation.
Can Puppeteer rotate proxies per request? Not cleanly at the browser tunnel level. For hard rotation, use a fresh browser or a fresh isolated task boundary.
Why does Puppeteer still use the old proxy? Chromium keeps sockets open and reuses them, so close the browser between hard rotations.
Why does auth slow down Puppeteer? The authentication path uses request interception behind the scenes, which can affect throughput.
Does an http:// proxy work for HTTPS sites in Puppeteer? Yes. Chromium opens a CONNECT tunnel through the proxy and runs TLS inside it, so one http://host:port line covers both http and https targets.
Can Puppeteer use SOCKS5 with a username and password? No. Chromium never sends credentials to SOCKS proxies, so an authenticated gateway needs the http form plus page.authenticate().
Production checks
- Keep credentials out of CLI args.
- Use one browser per hard proxy rotation.
- Set timeouts per navigation.
- Log status code and proxy label together.
- Stop retrying when the response is a stable block.