We use cookies to enhance user experience, personalize content, and analyze traffic. Cookie Policy

← Back to all articles

Puppeteer Proxy: Setup, page.authenticate, and Rotation

Set a Puppeteer proxy with --proxy-server or createBrowserContext, log in with page.authenticate(), and rotate proxies with one browser context per proxy.

by Unknown Proxies

14 min read

June 25, 2026

Updated October 17, 2026

Puppeteer Proxy: Setup, page.authenticate, and Rotation

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");

Puppeteer proxy request flow from Node.js to Chrome, proxy authentication, proxy exit, and target website

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.

Puppeteer proxy setup levels comparing browser launch proxy, context proxy, and page authentication

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:

  1. One context per page. Create a context with its own proxyServer and open a single page in it. This is the clean answer and needs no extra packages.
  2. Interception libraries such as puppeteer-page-proxy or Gajus's puppeteer-proxy. These intercept each request in Puppeteer and re-send it from Node.js through the proxy you choose. The puppeteer-proxy README 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.
  3. 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's Server class). This works, but you now own CONNECT tunnels, DNS, retries, and logging.

For most scraping and QA work, option 1 is enough.

Decision flow for choosing launch proxy, browser context proxy, or a new browser in Puppeteer

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:

Puppeteer proxy rotation checklist for session boundaries, sticky flows, pacing, and logging

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

  1. Test the proxy with curl -x.
  2. Launch one browser with one proxy and load an IP echo page.
  3. Add page.authenticate() only after the unauthenticated route is correct.
  4. Run with headless: false to see auth prompts or Chrome error pages.
  5. Check the bypass list.
  6. 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.

About the Author

Unknown Proxies

Proxy Infrastructure Team

Stay Unknown

High-performance dedicated proxies optimized for speed and reliability. Get uncompromising quality, 99.9% uptime, and unmatched support. Stay Unknown.

Explore Plans