To use a Puppeteer proxy, launch Chrome with --proxy-server=http://host:port (or set proxyServer on a browser context). If the proxy needs a login, call page.authenticate({ username, password }) before the first page.goto(). To rotate proxies, give each proxy its own browser context and close it when the task ends. The API names below match the Puppeteer docs as of October 2026.
Quick answer:
const browser = await puppeteer.launch({
args: ["--proxy-server=http://proxy.example.com:8080"],
});
const page = await browser.newPage();
await page.authenticate({ username: "USER", password: "PASS" });
await page.goto("https://example.com");
- Chrome ignores
user:pass@inside--proxy-server. Credentials only work throughpage.authenticate(). page.authenticate()does not set the proxy. It only answers the proxy's 407 challenge.- Call it on every page that uses the proxy, before navigation.
- To rotate, use one browser context per proxy with
browser.createBrowserContext({ proxyServer }). A page cannot switch proxies on its own.

Set the proxy with --proxy-server
Chrome handles proxy routing, so Puppeteer passes the proxy as a Chrome launch argument:
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({
args: ["--proxy-server=http://proxy.example.com:8080"],
});
const page = await browser.newPage();
await page.goto("https://ipv4.unknownproxies.com/ip");
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
If the page prints your own IP, Chrome did not get the flag, the format is wrong, or the host matched a bypass rule.
Accepted --proxy-server values:
| Value | Meaning |
|---|---|
http://host:port |
HTTP proxy. Also used for HTTPS sites through CONNECT. |
host:port |
Same as http://host:port |
https://host:port |
TLS connection to the proxy itself. Only use it if your provider offers this. |
socks5://host:port |
SOCKS5 proxy. Chrome does not support SOCKS5 with a username and password. |
http://user:pass@host:port |
Does not authenticate. Chrome ignores the credentials. |
HTTP and HTTPS proxy URLs
An http:// proxy URL carries HTTPS traffic too. For an HTTPS site, Chrome asks the proxy for a CONNECT tunnel and does the TLS handshake with the site through it. The https:// scheme in --proxy-server is different. It means Chrome talks to the proxy itself over TLS, which only works if your provider runs an HTTPS proxy endpoint. If a provider gives you an http:// endpoint, keep it as http:// even when you scrape https:// pages.
The flag applies to the whole browser process. You cannot change it after launch. To use a different proxy, use a browser context proxy (next section) or launch a new browser.
If your provider gives host:port:username:password, split it into --proxy-server=http://host:port and a { username, password } object. The proxy formatter converts whole lists between formats.

page.authenticate() for proxy authentication
The signature in the Puppeteer Page.authenticate() docs is:
page.authenticate(credentials: Credentials | null): Promise<void>
// Credentials = { username: string; password: string }
Full example with credentials from environment variables:
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({
args: ["--proxy-server=http://proxy.example.com:8080"],
});
const page = await browser.newPage();
await page.authenticate({
username: process.env.PROXY_USERNAME ?? "",
password: process.env.PROXY_PASSWORD ?? "",
});
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
await browser.close();
How it behaves:
| Behavior | Detail |
|---|---|
| What it does | Supplies credentials when the proxy (or a site) answers with an HTTP auth challenge, such as 407 Proxy Authentication Required |
| What it does not do | It does not choose a proxy. The proxy still comes from --proxy-server or proxyServer. |
| Scope | One page. Call it again for each new page, including popups. |
| Timing | Before the first page.goto() through the proxy |
| Side effect | Puppeteer turns on request interception for the page, which the docs note "might affect performance" |
| Disable | await page.authenticate(null) |
Because Chrome waits for a 407 challenge before it sends credentials, a proxy that expects a Proxy-Authorization header on the very first request will not work with page.authenticate(). Use IP allowlisting on that proxy, or a local forwarder such as the proxy-chain npm package, shown in the rotation section below.
If authentication still fails, test the same credentials outside Puppeteer:
curl -x "http://USERNAME:PASSWORD@proxy.example.com:8080" https://ipv4.unknownproxies.com/ip
If curl fails, fix the host, port, credentials, allowlist, or plan status first. Our 407 Proxy Authentication Required guide lists the usual causes. If curl works and Puppeteer fails, check that page.authenticate() runs before navigation and on the right page.
Browser context proxy: createBrowserContext({ proxyServer })
Puppeteer can also set a proxy per browser context. Each context has its own cookies, storage, and cache. BrowserContextOptions has two proxy fields:
| Option | Type | Description from the docs |
|---|---|---|
proxyServer |
string |
"Proxy server with optional port to use for all requests. Username and password can be set in Page.authenticate." |
proxyBypassList |
string[] |
"Bypass the proxy for the given list of hosts." |
const browser = await puppeteer.launch();
const context = await browser.createBrowserContext({
proxyServer: "http://proxy.example.com:8080",
proxyBypassList: ["localhost", "127.0.0.1"],
});
const page = await context.newPage();
await page.authenticate({
username: process.env.PROXY_USERNAME ?? "",
password: process.env.PROXY_PASSWORD ?? "",
});
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
await context.close();
await browser.close();
createBrowserContext() is the current name. Puppeteer versions before v22 called it createIncognitoBrowserContext(). Check your installed version if the method is missing.
Use launch args when the whole run shares one proxy. Use a context proxy when you need several isolated identities in one browser process.
Proxy for downloading Chrome (HTTP_PROXY)
The --proxy-server flag only affects pages. Puppeteer also downloads a browser when you install it, and that download follows the HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables. The Puppeteer configuration docs say downloading through a proxy requires the proxy-agent package, installed as an optional peer dependency. These variables are ignored by puppeteer-core, which does not download a browser.
Proxy bypass and localhost
To keep some hosts direct, pass --proxy-bypass-list:
const browser = await puppeteer.launch({
args: [
"--proxy-server=http://proxy.example.com:8080",
"--proxy-bypass-list=localhost;127.0.0.1;*.internal.example.com",
],
});
Chrome already skips the proxy for localhost and loopback addresses by default. If you need loopback traffic to go through the proxy (for example, to test a local proxy), add <-loopback> to the bypass list. Details are in the Chromium proxy docs.
If you bypass the target host by accident, your own IP reaches the site.
How to rotate proxies in Puppeteer
Rotate at the browser context boundary: one proxy, one context, one bounded task, then close the context. You can run many contexts in one Chrome process.
Rotation options at a glance
| Method | Isolation | Cost | Use when |
|---|---|---|---|
New context per proxy (createBrowserContext) |
Cookies, storage, cache, and IP per context | Low. One Chrome process. | Default for rotation on current Puppeteer |
New browser per proxy (--proxy-server) |
Full process isolation | High CPU and memory per proxy | Puppeteer without context proxies, or strict isolation needs |
| Provider-side rotation (rotating gateway) | One endpoint, new IP per connection | Lowest code effort | Independent requests with no session state |
| Interception library (per request) | None. Same cookies, different IPs. | Changes the network client | Rarely. See the caveats below. |
| Local routing proxy (per request) | You decide | You own the networking code | You need rules like "these hosts via proxy A" |
Rotate with one browser context per proxy
This is the pattern to use on current Puppeteer. One Chrome process, a fresh context per proxy, and page.authenticate() for credentials:
import puppeteer from "puppeteer";
type ProxyConfig = {
server: string; // "http://host:port"
username?: string;
password?: string;
};
const proxies: ProxyConfig[] = [
{ server: "http://proxy-1.example.com:8080", username: "user", password: "pass" },
{ server: "http://proxy-2.example.com:8080", username: "user", password: "pass" },
{ server: "http://proxy-3.example.com:8080", username: "user", password: "pass" },
];
const browser = await puppeteer.launch();
for (const proxy of proxies) {
const context = await browser.createBrowserContext({ proxyServer: proxy.server });
try {
const page = await context.newPage();
if (proxy.username && proxy.password) {
await page.authenticate({ username: proxy.username, password: proxy.password });
}
await page.goto("https://ipv4.unknownproxies.com/ip", { timeout: 30_000 });
console.log(proxy.server, await page.evaluate(() => document.body.innerText));
// Run one bounded task for this proxy identity.
} finally {
await context.close();
}
}
await browser.close();
Closing the context drops its cookies, storage, and cache, so the next proxy starts clean. On Puppeteer versions before v22, the method was createIncognitoBrowserContext().
Run a concurrent proxy pool
For more throughput, run several contexts at the same time, with a fixed limit. Each worker takes the next URL and the next proxy:
import puppeteer, { type Browser } from "puppeteer";
const urls = ["https://example.com/a", "https://example.com/b", "https://example.com/c"];
const proxies = [
{ server: "http://proxy-1.example.com:8080", username: "user", password: "pass" },
{ server: "http://proxy-2.example.com:8080", username: "user", password: "pass" },
];
const CONCURRENCY = 2;
let nextUrl = 0;
let nextProxy = 0;
async function worker(browser: Browser) {
while (nextUrl < urls.length) {
const url = urls[nextUrl++];
const proxy = proxies[nextProxy++ % proxies.length];
const context = await browser.createBrowserContext({ proxyServer: proxy.server });
try {
const page = await context.newPage();
await page.authenticate({ username: proxy.username, password: proxy.password });
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 });
console.log(url, proxy.server, response?.status());
} catch (err) {
console.error(url, proxy.server, (err as Error).message);
} finally {
await context.close();
}
}
}
const browser = await puppeteer.launch();
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker(browser)));
await browser.close();
Keep CONCURRENCY low at first. One page.goto() loads HTML, scripts, images, fonts, and API calls through the same proxy, so ten pages can mean hundreds of requests.
Rotate with one browser per proxy
If your Puppeteer version has no context proxy options, or you want each identity in its own process, launch a browser per proxy:
for (const proxy of proxies) {
const browser = await puppeteer.launch({ args: [`--proxy-server=${proxy.server}`] });
try {
const page = await browser.newPage();
await page.authenticate({ username: proxy.username, password: proxy.password });
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
} finally {
await browser.close();
}
}
This costs a full Chrome start per proxy (often hundreds of MB of RAM each). It is the simplest model to reason about.
Authenticated rotation without page.authenticate()
page.authenticate() must run on every page, including popups. If that gets awkward, run a local forwarder that adds the credentials for Chrome. The proxy-chain npm package does this:
import puppeteer from "puppeteer";
import { anonymizeProxy, closeAnonymizedProxy } from "proxy-chain";
const localProxy = await anonymizeProxy("http://user:pass@proxy.example.com:8080");
// localProxy is something like "http://127.0.0.1:45678"
const browser = await puppeteer.launch({ args: [`--proxy-server=${localProxy}`] });
// ...pages need no page.authenticate() call...
await browser.close();
await closeAnonymizedProxy(localProxy, true);
The forwarder also helps with proxies that expect the Proxy-Authorization header on the first request, which Chrome does not send without a 407 challenge. It is also how you give an authenticated proxy to an AI agent that drives Chrome through the Puppeteer MCP server, which never calls page.authenticate(). The Puppeteer MCP proxy guide covers that setup. anonymizeProxy() is documented for HTTP and HTTPS upstream proxies. You can pass the local URL to createBrowserContext({ proxyServer }) too.
Per-page proxies: what actually works
A Puppeteer Page is not an independent network stack. It lives inside a browser context, and cookies, cache, service workers, and connection pools belong to the context. Opening a new page in the same context does not give you a new IP, and page.authenticate() changes credentials, not the route.
People search for a per-page proxy because they want two tabs with two IPs. Here are the real options:
- One context per page. Create a context with its own
proxyServerand open a single page in it. This is the clean answer and needs no extra packages. - Interception libraries such as
puppeteer-page-proxyor Gajus'spuppeteer-proxy. These intercept each request in Puppeteer and re-send it from Node.js through the proxy you choose. Thepuppeteer-proxyREADME says routing through Node "will introduce additional latency", and Chrome is no longer the network client for those requests, so the TLS fingerprint changes. Cookies also stay shared across "different" IPs. Check a package's recent releases before you depend on it. - A local routing proxy. Point Chrome at a proxy you run on localhost, and choose the upstream per request in your code (for example with
proxy-chain'sServerclass). This works, but you now own CONNECT tunnels, DNS, retries, and logging.
For most scraping and QA work, option 1 is enough.

Rotating gateways and sticky sessions
Many residential providers give you a gateway that rotates the exit IP for you, plus a sticky option that holds one IP for a session. With a rotating gateway, one --proxy-server value gives a new IP per connection. That is fine for independent page loads. It breaks flows that need one visitor, because Chrome reuses connections and opens new ones unpredictably.
Rules that hold up:
- Rotate between independent tasks, not between clicks in the same task.
- Use a sticky session for login, cart, checkout, and account flows. Our guide to sticky vs rotating proxies covers the tradeoff, and the backconnect proxy guide explains how a gateway assigns IPs.
- Tie one sticky session to one browser context, and close both together.

Retries, backoff, and retiring bad proxies
Rotation is not a retry strategy by itself. Track results per proxy and react to the type of failure:
| Result | Likely meaning | Action |
|---|---|---|
net::ERR_PROXY_CONNECTION_FAILED or timeout |
Proxy unreachable or overloaded | Retry once on another proxy, mark this one unhealthy |
| 407 | Credentials wrong or missing | Fix config. Do not retry on the same proxy. |
| 429 | Target rate limit | Slow down. More proxies at the same rate rarely help. |
| 403 | Target policy, reputation, or fingerprint | Check headers and behavior before you rotate |
| 200 with a CAPTCHA page | Soft block | Lower concurrency, check session handling |
Add jitter to delays, cap retries per URL, and stop when a target clearly blocks the pattern. If an LLM agent picks the clicks instead of your script, proxies for AI agents covers session mapping for browser agents.
Quick debugging checklist
- Test the proxy with
curl -x. - Launch one browser with one proxy and load an IP echo page.
- Add
page.authenticate()only after the unauthenticated route is correct. - Run with
headless: falseto see auth prompts or Chrome error pages. - Check the bypass list.
- Log the proxy ID, never the full URL with the password.
For net::ERR_PROXY_CONNECTION_FAILED, net::ERR_TUNNEL_CONNECTION_FAILED, timeouts, and other failures, use the Puppeteer proxy error troubleshooting guide.
Which proxy type to use
Residential proxies fit localized public-page checks and rotating sessions. ISP proxies fit flows that need the same dedicated IP over time, such as account checks, QA, and monitoring. Unknown Proxies offers residential proxies and dedicated ISP proxy plans. For retail checkout and sneaker flows, where one IP has to last the whole session, the sneaker proxy guide compares the two. Both use HTTP endpoints with a username and password, which matches the --proxy-server plus page.authenticate() pattern above. The dashboard generates residential lines as rotating or sticky. Each sticky line carries its own session ID, so each line maps to one browser context in the rotation code.
FAQ
How do I use page.authenticate for proxy authentication in Puppeteer?
Launch with --proxy-server=http://host:port, open a page, and call await page.authenticate({ username, password }) before page.goto(). Puppeteer then answers the proxy's 407 challenge with those credentials. It only supplies credentials. The proxy itself comes from --proxy-server or proxyServer. Call it on every new page, including popups.
Can I put the username and password in --proxy-server?
No. Chrome ignores credentials in the --proxy-server value. Use page.authenticate(), IP allowlisting, or a local forwarder.
Does Puppeteer support SOCKS5 proxies with a password?
Chrome supports socks5://host:port without credentials. It does not support SOCKS5 username and password authentication. Use your provider's HTTP endpoint or a local forwarder instead.
How do I set a different proxy per browser context or page in Puppeteer?
Call browser.createBrowserContext({ proxyServer: "http://host:port" }) for each context, then page.authenticate() on its pages if the proxy needs credentials. A page cannot have its own proxy inside a shared context, so open each page in its own context. Interception libraries can route single requests elsewhere, but they replace Chrome's network stack for those requests.
How do I rotate proxies in Puppeteer?
Create a new browser context for each proxy with browser.createBrowserContext({ proxyServer }), authenticate the page, run one task, and close the context. On old Puppeteer versions, launch a new browser per proxy with --proxy-server.
Can I change the proxy without restarting the browser?
Yes, with context proxies. Close the old context and create a new one with a different proxyServer. The --proxy-server launch flag cannot change while the browser runs.
Should I rotate proxies during a login or checkout?
No. Keep one sticky proxy and one context for the whole flow. Changing IPs mid-session makes one visitor look like several and often triggers re-authentication or blocks.
Final thoughts
Set the route with --proxy-server or proxyServer, then authenticate each page with page.authenticate() before navigation. Keep credentials out of the proxy URL and out of your logs. When you need many identities, rotate at the context boundary with a concurrency limit, retire proxies that fail at the connection level, and slow down on 429s instead of adding IPs.
For browser automation comparisons, see Playwright proxy setup and Python Selenium proxy setup.
Technical references: Puppeteer BrowserContextOptions documentation, Puppeteer Page.authenticate() documentation, Chromium proxy support notes, and the proxy-chain README.
