A Puppeteer proxy takes two steps: start Chrome with --proxy-server=http://HOST:PORT, then call page.authenticate() with the username and password before the first navigation. Chrome itself will not take credentials in the flag, so the second step is not optional.
const browser = await puppeteer.launch({ args: ['--proxy-server=http://HOST:PORT'] });
const page = await browser.newPage();
await page.authenticate({ username: 'USERNAME', password: 'PASSWORD' });
await page.goto('https://api.ipify.org?format=json');
The snippets on this page ran on 2026-09-29 with Puppeteer 25.12 driving Chrome 154, against two local authenticating HTTP proxies and a SOCKS5 proxy that enforces credentials.
Before you start: copy your proxy details
- Open your order in the dashboard at https://app.proxyhive.io.
- Copy HOST, PORT, USERNAME and PASSWORD. Each ISP or datacenter IP is its own endpoint, with its own HTTP, HTTPS and SOCKS5 ports on the order. Use the HTTP port with Puppeteer unless you allowlist your IP.
- Export them:
export PROXY_SERVER="http://HOST:PORT" PROXY_USER=USERNAME PROXY_PASS=PASSWORD
For long-lived browser sessions, static ISP proxies keep one address for the whole term, sold from a single IP.
Puppeteer proxy with --proxy-server
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
args: [`--proxy-server=${process.env.PROXY_SERVER}`],
});
const page = await browser.newPage();
await page.authenticate({
username: process.env.PROXY_USER,
password: process.env.PROXY_PASS,
});
await page.goto('https://api.ipify.org?format=json');
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
--proxy-server is a Chrome flag, so it applies to the whole browser. Add --proxy-bypass-list=localhost;*.internal.example for hosts that should go direct.
Puppeteer proxy authentication with page.authenticate
What we saw with each way of passing credentials:
| Approach | Result in our run |
|---|---|
--proxy-server=http://HOST:PORT plus page.authenticate() | Works |
--proxy-server=http://HOST:PORT, no authenticate | net::ERR_INVALID_AUTH_CREDENTIALS |
page.authenticate() with a wrong password | No exception: goto resolves with status 407 |
--proxy-server=http://USER:PASS@HOST:PORT | net::ERR_NO_SUPPORTED_PROXIES |
Two things to know about page.authenticate():
- It is per page. A page opened later, including a popup, needs its own call.
- The Puppeteer API reference notes it turns on request interception behind the scenes, which can slow pages a little. The allowlist below avoids that.
IP allowlist: no credentials at all
On a server with a fixed public IP, switch the proxy IP to allowlist authentication on the order in the dashboard and add the server's address. Then --proxy-server=http://HOST:PORT is the whole setup, with no authenticate call, no interception, and nothing secret in your code. For browsers this is the cleanest option.
A proxy per browser context: createBrowserContext({ proxyServer })
To run several IPs from one Chrome process, give each browser context its own proxy:
import puppeteer from 'puppeteer';
const servers = process.env.PROXY_SERVERS.split(',');
const browser = await puppeteer.launch();
for (const proxyServer of servers) {
const context = await browser.createBrowserContext({ proxyServer });
const page = await context.newPage();
await page.authenticate({
username: process.env.PROXY_USER,
password: process.env.PROXY_PASS,
});
await page.goto('https://api.ipify.org?format=json');
console.log(proxyServer, await page.evaluate(() => document.body.innerText));
await context.close();
}
await browser.close();
Our run sent the two contexts through their two proxies from one browser. createBrowserContext() is the current name; code written for Puppeteer before v22 calls it createIncognitoBrowserContext(). Each context has separate cookies and storage, so one IP's session never bleeds into another's.
This rotates per context, not per request, which suits browsers: a login that hops addresses mid-session looks worse than one that stays put. Rotating vs static proxies covers when per-request rotation is still the right call.
Puppeteer SOCKS5 proxy: no authentication
--proxy-server=socks5://HOST:PORT works for an allowlisted IP. With credentials it does not: our SOCKS server logged that Chrome offered only the "no authentication" method, and the page failed with net::ERR_SOCKS_CONNECTION_FAILED. page.authenticate() never gets a chance, because SOCKS auth happens before any HTTP. So:
- HTTP port with
page.authenticate(), or - allowlist your IP and use
socks5://HOST:SOCKS5_PORTwith no credentials.
HTTP vs SOCKS5 proxies explains why the HTTP port loses a browser nothing.
Verify the exit IP
The snippets print the address from https://api.ipify.org?format=json. Compare it with the IP on your order. If it shows your own address, the flag did not reach Chrome: check args and that you are not connecting to an already-running browser with puppeteer.connect(), which ignores launch flags.
Timeouts and retries
page.setDefaultNavigationTimeout(45_000);
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await page.goto(url, { waitUntil: 'domcontentloaded' });
break;
} catch (err) {
if (attempt === 3) throw err;
}
}
The first navigation in a context is the slowest, since it pays for the proxy handshake. domcontentloaded avoids waiting on third-party requests that never settle.
Common Puppeteer proxy errors
| Error | Cause | Fix |
|---|---|---|
net::ERR_INVALID_AUTH_CREDENTIALS | No page.authenticate() on this page | Call it on every page, popups included |
goto resolves, but response.status() is 407 | Wrong username or password passed to authenticate | Re-copy the credentials; check the status, since nothing throws |
net::ERR_NO_SUPPORTED_PROXIES | Credentials inside --proxy-server | Scheme, host and port only |
net::ERR_SOCKS_CONNECTION_FAILED | SOCKS5 with credentials | HTTP port, or allowlist |
net::ERR_PROXY_CONNECTION_FAILED | Wrong host or port | Re-copy the port for your protocol |
For status codes returned by the site, see proxy error codes.
Next steps
- Playwright accepts proxy credentials directly and needs no
authenticatestep. - Plain HTTP requests from Node: undici and axios.
- Connection reference: the docs.