Puppeteer proxy with authentication: --proxy-server, page.authenticate and per-context proxies
Set up an authenticated proxy in Puppeteer the way Chrome accepts it: --proxy-server for the address, page.authenticate for the credentials, a different proxy per browser context, a SOCKS5 workaround, and the net:: errors behind each mistake.

Chrome takes a proxy address as a command-line flag, but it will not take a username and password in that flag. Puppeteer therefore splits the job in two: the address goes in the --proxy-server flag, the credentials go through page.authenticate(). Get that split right and everything else, from per-context proxies to sticky sessions, follows. Everything below was tested with Puppeteer 25.16 and Chrome 155.
The basic setup
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({
args: ["--proxy-server=http://geo.gw.nyxon.sh:8080"],
});
const page = await browser.newPage();
await page.authenticate({ username: "USERNAME-country-us", password: "PASSWORD" });
const res = await page.goto("https://api.ipify.org?format=json");
console.log(res.status(), await page.evaluate(() => document.body.innerText));
await browser.close();Call page.authenticate() before the first goto(), and call it on every page you open: the credentials belong to the page, not the browser. Targeting options such as -country-us or -session-abc123 go in the username, as in any other client.
Puppeteer's documentation notes that authenticate() turns on request interception behind the scenes to do its work, which "might affect performance". It is still the supported way to pass proxy credentials to Chrome.
Three mistakes and the errors they produce
Credentials inside the flag. This looks reasonable and does not work:
--proxy-server=http://USERNAME:[email protected]:8080Chrome rejects it and every navigation fails with net::ERR_NO_SUPPORTED_PROXIES.
Forgetting to authenticate. Without page.authenticate(), the proxy asks for credentials, nobody answers, and goto() throws net::ERR_INVALID_AUTH_CREDENTIALS.
A wrong password. This one does not throw. In our test goto() resolved normally with a response whose status was 407, and the page held Chrome's "This page isn't working" error screen. A script that does not check the status will happily scrape the error page. Always check it:
const res = await page.goto(url);
if (!res.ok()) throw new Error(`HTTP ${res.status()} for ${url}`);A different proxy for each browser context
A browser context is an isolated session inside one browser, with its own cookies and storage. browser.createBrowserContext() accepts a proxyServer option, and Puppeteer's documentation says the username and password for it are set with page.authenticate(). So each context gets its own proxy address, and each of its pages gets its own credentials:
import puppeteer from "puppeteer";
import { randomBytes } from "node:crypto";
const browser = await puppeteer.launch();
async function openSession(country) {
const context = await browser.createBrowserContext({ proxyServer: "http://geo.gw.nyxon.sh:8080" });
const page = await context.newPage();
const sid = randomBytes(4).toString("hex");
await page.authenticate({ username: `USERNAME-country-${country}-session-${sid}-time-20`, password: "PASSWORD" });
return { context, page };
}
for (const country of ["us", "de", "gb"]) {
const { context, page } = await openSession(country);
await page.goto("https://ipinfo.io/json");
console.log(country, await page.evaluate(() => document.body.innerText));
await context.close();
}
await browser.close();The browser itself was launched without --proxy-server. In our test the context proxies worked on their own, and each session id came through the proxy as a separate user, so a separate exit IP.
Before Puppeteer 22 this method was called createIncognitoBrowserContext(). Version 22 renamed it, so older tutorials and code use the old name.
Rotating or sticky
A page load is many connections, not one. With a rotating username, every new connection can leave through a different exit, so one page view can arrive from several IPs. That is fine for anonymous page fetches. For a login, a checkout or a multi-step form, put a session id in the username, as above, and keep -time- (in minutes, up to 120) longer than the task. When you want a fresh IP, close the context and open a new one with a new session id. The sessions guide explains the trade-off.
SOCKS5: use the HTTP port, or a local forwarder
Chrome can use SOCKS5 proxies, but it cannot log in to one. page.authenticate() only answers HTTP proxy challenges, so a SOCKS5 proxy that requires credentials fails with net::ERR_SOCKS_CONNECTION_FAILED.
The easy fix is the HTTP port of the same product, geo.gw.nyxon.sh:8080 for residential. It carries HTTPS sites through an encrypted tunnel, so there is nothing to gain from SOCKS5 in a browser. The SOCKS5 vs HTTP comparison has the details.
If you do need SOCKS5, or you drive Chrome from a tool that cannot call page.authenticate(), run a small local forwarder. The proxy-chain package starts a proxy on 127.0.0.1 that needs no credentials and forwards everything to your authenticated upstream:
import puppeteer from "puppeteer";
import { anonymizeProxy, closeAnonymizedProxy } from "proxy-chain";
const local = await anonymizeProxy("socks5://USERNAME:[email protected]:1080");
const browser = await puppeteer.launch({ args: [`--proxy-server=${local}`] });
const page = await browser.newPage();
await page.goto("https://api.ipify.org?format=json");
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
await closeAnonymizedProxy(local, true);No page.authenticate() is needed, because Chrome talks to the local forwarder and the forwarder logs in upstream. We tested it with both an HTTP and a SOCKS5 upstream. Install it with npm i proxy-chain.
Bypassing the proxy
Chrome already sends localhost and 127.0.0.1 direct, even with --proxy-server set; in our test a local server was never proxied. For any other host that should skip the proxy, such as an internal API, --proxy-bypass-list takes a semicolon-separated list, with * as a wildcard:
const browser = await puppeteer.launch({
args: [
"--proxy-server=http://geo.gw.nyxon.sh:8080",
"--proxy-bypass-list=*.internal.example;status.example.com",
],
});For a context, use the proxyBypassList option, an array: createBrowserContext({ proxyServer, proxyBypassList: ["*.internal.example"] }).
Cut bandwidth with request interception
Proxy traffic is billed per gigabyte, and images, fonts and media are most of a page's weight. If you only need the HTML or the data, abort them in the browser:
await page.setRequestInterception(true);
page.on("request", req => {
if (["image", "media", "font"].includes(req.resourceType())) req.abort();
else req.continue();
});It works alongside page.authenticate(). Aborted requests never reach the proxy, so they cost nothing.
Error reference
What Chrome 155 reported under Puppeteer 25 for each problem. Puppeteer's error messages put a net:: prefix in front of each code, as in net::ERR_PROXY_CONNECTION_FAILED:
ERR_PROXY_CONNECTION_FAILED: wrong host or port, or the proxy is unreachable from this machine.ERR_NO_SUPPORTED_PROXIES: credentials, or an unsupported scheme, inside--proxy-server.ERR_INVALID_AUTH_CREDENTIALS: the proxy asked for credentials andpage.authenticate()was never called.- A 407 response with Chrome's error page:
page.authenticate()sent the wrong username or password. ERR_TUNNEL_CONNECTION_FAILED: the proxy refused to open the tunnel with an error status such as 403 or 502.ERR_SOCKS_CONNECTION_FAILED: SOCKS5 with credentials, which Chrome cannot do; use the HTTP port.
For what the status codes behind these mean, and how to tell a proxy error from a website error, see the proxy error codes guide.
FAQ
Can I set the proxy username and password in launch options? No. Chrome ignores credentials in --proxy-server and fails with net::ERR_NO_SUPPORTED_PROXIES. Use page.authenticate().
Can each page use a different proxy? Not natively. Proxies are set per browser or per browser context, so open one context per proxy. Third-party packages route single requests through different proxies, but they reimplement networking outside Chrome.
Does page.authenticate() work with SOCKS5? No. It only answers HTTP proxy challenges. Use the HTTP port, or a local forwarder such as proxy-chain.
How is Playwright different? Playwright takes the username and password directly in its proxy object, at launch or per context. See the Playwright proxy guide.
Try it
All of this works on any NyxonProxies plan. Residential gives you ASN and US city targeting and sticky sessions; ISP gives you one static IP for long sessions. Create an account, link Discord under Freebies for 250MB free, and copy the gateway details from the Generate page.