Back to blog

Guide

Puppeteer Proxy Setup: Launch Args, Authentication and Rotation

How to use a proxy with Puppeteer: the --proxy-server launch argument, why authentication needs a separate page.authenticate call, per-page rotation, bypass lists and verification.

Puppeteer takes the proxy as a Chromium launch argument, and — unlike Playwright — will not accept credentials there. Authentication is a separate call on every page. Getting that second part wrong is the most common Puppeteer proxy problem, and it shows up as a 407 that looks like bad credentials when the credentials are fine.

import puppeteer from "puppeteer";
 
const browser = await puppeteer.launch({
  args: ["--proxy-server=http://gateway.example.com:8000"],
});
 
const page = await browser.newPage();
 
// Required for authenticated proxies — credentials cannot go in the launch arg
await page.authenticate({
  username: process.env.PROXY_USER,
  password: process.env.PROXY_PASS,
});
 
await page.goto("https://api.ipify.org");
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();

Puppeteer Proxy Setup

Why authentication is separate

Chromium's --proxy-server flag accepts a host and port. Credentials embedded as http://user:pass@host:port are ignored — the browser does not parse them from that flag. So the proxy challenges with 407, and nothing in your code answers.

page.authenticate() registers the credentials with the page's network layer so it can respond to that challenge. Three rules:

  1. Call it on every page. It does not inherit. A second newPage() is unauthenticated until you authenticate it too.
  2. Call it before the first goto(). Authenticating after navigation has already failed does not retroactively fix it.
  3. It applies per page, not per browser. There is no browser-level equivalent.
async function newAuthedPage(browser) {
  const page = await browser.newPage();
  await page.authenticate({
    username: process.env.PROXY_USER,
    password: process.env.PROXY_PASS,
  });
  return page;
}

Wrapping it like that is worth doing immediately, because the failure mode when you forget is confusing.

Bypass lists

const browser = await puppeteer.launch({
  args: [
    "--proxy-server=http://gateway.example.com:8000",
    "--proxy-bypass-list=localhost;127.0.0.1;*.internal.example.com",
  ],
});

Note the separator is a semicolon, not a comma — different from most other tools, and a silent failure if you get it wrong.

SOCKS5

const browser = await puppeteer.launch({
  args: ["--proxy-server=socks5://gateway.example.com:1080"],
});

Chromium does not support SOCKS5 username and password authentication through this flag, and page.authenticate() does not cover SOCKS auth either. If you need SOCKS5, use IP allowlisting rather than credentials — see proxy authentication methods.

Rotation

Because the launch argument is browser-wide, rotation means new browsers:

async function fetchWithFreshIp(url) {
  const browser = await puppeteer.launch({
    args: ["--proxy-server=http://gateway.example.com:8000"],
  });
  try {
    const page = await browser.newPage();
    await page.authenticate({
      username: process.env.PROXY_USER,
      password: process.env.PROXY_PASS,
    });
    await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
    return await page.content();
  } finally {
    await browser.close();
  }
}

A rotating gateway assigns a new exit per connection, so each browser gets a different IP. The cost is real: launching Chromium is expensive in both time and memory, so do not do this per request if you can batch.

For sticky sessions, encode the session in the username and keep one browser for the whole flow:

const sessionId = Math.random().toString(36).slice(2, 10);
 
const page = await browser.newPage();
await page.authenticate({
  username: `${process.env.PROXY_USER}-session-${sessionId}`,
  password: process.env.PROXY_PASS,
});
// every request from this page exits from one IP

Exact parameter format is provider-specific. Sticky sessions covers when you need them — essentially anything with server-side state.

Reducing bandwidth

A real browser fetches everything: images, fonts, stylesheets, trackers. On per-gigabyte billing that adds up fast.

await page.setRequestInterception(true);
page.on("request", (req) => {
  const blocked = ["image", "font", "stylesheet", "media"];
  if (blocked.includes(req.resourceType())) req.abort();
  else req.continue();
});

This typically removes most of the page weight. Only do it when you do not need rendering fidelity — blocking stylesheets can change what JavaScript-driven layout produces.

Matching the browser to the exit

As important as the proxy itself:

await page.setExtraHTTPHeaders({ "Accept-Language": "de-DE,de;q=0.9" });
await page.emulateTimezone("Europe/Berlin");

A German exit IP reporting a US locale and a New York timezone is internally contradictory, and detection systems evaluate precisely that. Setting these costs nothing and frequently matters more than IP quality — see anti-bot detection in 2026.

Verifying the exit

await page.goto("https://api.ipify.org");
const ip = await page.evaluate(() => document.body.innerText.trim());
console.log(`exit IP: ${ip}`);

Run this where the code actually runs. Locally it passes regardless, because your machine already has a residential IP; the failure only appears once deployed to a datacenter host.

Common errors

407 despite correct credentials. page.authenticate() missing, called after goto(), or not called on that specific page.

net::ERR_TUNNEL_CONNECTION_FAILED. Proxy refused CONNECT for that target.

net::ERR_PROXY_CONNECTION_FAILED. Wrong host or port, or proxy unreachable.

Bypass list ignored. You used commas. Chromium wants semicolons.

SOCKS5 auth failing. Not supported through this path. Use IP allowlisting.

Blocked headless, fine headed. The browser is the signal, not the IP. No proxy fixes this.

Slow pages. Assets loading through the proxy. Intercept and block what you do not need.

Which proxy type

Residential for protected targets, datacenter for volume against unprotected ones. Puppeteer drives a real browser, so the TLS and browser fingerprint layers are already realistic — which makes residential IPs more effective here than behind a raw HTTP client, because nothing else is contradicting them.

FlameProxies supports HTTP(S) and SOCKS5 across 80M-plus residential IPs in 180-plus countries at $0.50/GB, dropping to $0.45/GB above 1TB, with unlimited concurrent sessions. Rates are on the pricing page.

For the Playwright equivalent — which takes credentials directly at launch and supports per-context proxies — see Playwright proxy setup.

Frequently asked questions

How do I set a proxy in Puppeteer?
Pass it as a Chromium launch argument: puppeteer.launch({args: ['--proxy-server=http://host:port']}). Unlike Playwright, Puppeteer has no proxy option object, so the proxy is configured through the browser's own command-line flag.
How do I use an authenticated proxy with Puppeteer?
Credentials cannot go in the --proxy-server argument — Chromium ignores them there. You have to call await page.authenticate({username, password}) on each page before navigating. This is the single biggest difference from Playwright and the most common reason a Puppeteer proxy setup returns 407.
Can I use different proxies per page in Puppeteer?
Not with the launch argument, because it applies to the whole browser. To run different exits concurrently you either launch multiple browsers, or use browser contexts with an extension-based proxy switcher. Launching one browser per proxy is simpler and usually what people do, at the cost of memory.
Why does page.authenticate not work in Puppeteer?
It has to be called on every page, before navigation, and it does not carry across pages created afterwards. If you authenticate one page then open a second, the second is unauthenticated. Call it immediately after every newPage(), and before the first goto().
Does Puppeteer support SOCKS5 proxies?
Yes, through the same launch argument with a socks5 scheme: --proxy-server=socks5://host:port. Chromium does not support SOCKS5 username and password authentication through that flag, though, so SOCKS5 with credentials generally needs IP allowlisting instead.
Why is my Puppeteer script blocked with a working proxy?
Headless Chromium is detectable independently of the IP, and the browser's locale and timezone still have to agree with the exit country. If a target challenges you headless but not headed, the proxy is working and the browser is the signal being caught.