Guide

How to Use Proxies With Puppeteer

How to use proxies with Puppeteer: the proxy-server launch arg, authenticated proxies with page.authenticate and proxy-chain, per-context proxies, and rotating a pool.

HProxy Team··5 min read
HProxy.Guide

Skip the dead lists.

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump.

Open the free proxy list

Puppeteer routes through a proxy with a single launch flag, and then hits one wall that stops most people: Chromium's --proxy-server argument does not accept a username and password, so the moment your proxy needs authentication the obvious approach silently fails. This guide covers proxies with Puppeteer end to end: the launch argument, authenticated proxies with both page.authenticate and the proxy-chain package, giving different pages different exits, rotating a pool, and verifying the exit IP so you know it actually took effect.

We run a proxy network and a live proxy checker, so the Puppeteer proxy question we answer most is why an authenticated proxy appears to be ignored. The answer is almost always the credentials-in-the-URL trap below. Every example runs on Puppeteer 22 or newer. Where one needs a live proxy, pull a fresh one from our free proxy API, which returns recently checked endpoints with no key.

How do you set a proxy in Puppeteer?

Pass --proxy-server as a launch argument. It applies to the whole browser, so every page opened from that instance uses the same exit:

import puppeteer from "puppeteer";

const browser = await puppeteer.launch({
  args: ["--proxy-server=http://203.0.113.7:8080"],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip");
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();

The scheme in the flag can be http://, https://, or socks5://. A SOCKS5 proxy needs no extra package on the Chromium side, which is one way Puppeteer is simpler than the Python clients.

The authentication trap

The instinct from every other tool is to put the login in the URL, user:pass@host:port. Chromium ignores the userinfo part of --proxy-server and connects unauthenticated, so the proxy answers with 407 Proxy Authentication Required and your navigation fails. This is the single most common Puppeteer proxy bug.

The built-in fix is page.authenticate, called before the navigation that triggers the challenge:

const browser = await puppeteer.launch({
  args: ["--proxy-server=http://203.0.113.7:8080"],
});
const page = await browser.newPage();
await page.authenticate({ username: "user", password: "pass" });
await page.goto("https://httpbin.org/ip");

page.authenticate works, but it is per page, so a script that opens many tabs has to call it on each one, and it interacts awkwardly with request interception. For anything beyond a single page, the sturdier answer is proxy-chain.

Why credentials in the proxy URL fail, and the two fixes
  1. user:pass@host in --proxy-server

    Chromium drops the login

  2. Proxy returns 407

    navigation fails

  3. page.authenticate

    answers 407, per page

  4. proxy-chain

    local proxy holds the login

Source: Chromium ignores userinfo in --proxy-server

Authenticated proxies with proxy-chain

The proxy-chain package starts a local proxy that carries your credentials and forwards to the upstream, so Chromium points at a plain http://127.0.0.1:PORT with no auth and never sees the 407:

import puppeteer from "puppeteer";
import { anonymizeProxy } from "proxy-chain";

const upstream = "http://user:pass@203.0.113.7:8080";
const local = await anonymizeProxy(upstream);   // -> http://127.0.0.1:xxxxx

const browser = await puppeteer.launch({ args: [`--proxy-server=${local}`] });
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip");
// ... work ...
await browser.close();
// closeAnonymizedProxy(local, true) when you are fully done

This is the pattern to standardise on. It handles auth once, works across every page in the browser, and gives you a place to swap the upstream when you rotate.

Different exits per page

There is no per-page proxy from a single browser, because --proxy-server is fixed at launch for the whole Chromium process. Two patterns get you different exits:

  • One browser per proxy. Launch a separate browser for each exit and open a batch of pages inside it. This is the simplest and most reliable, at the cost of a Chromium process per proxy.
  • Browser contexts. browser.createBrowserContext() isolates cookies and storage, and on recent versions accepts a proxyServer option so a context can carry its own exit. It is lighter than a whole browser but check it behaves on your Puppeteer version before relying on it.

For most rotation jobs, one browser per proxy reused for a batch of pages is the pattern that does not surprise you.

Rotating a pool

Because the proxy is set at launch, rotation means picking an exit per browser and retrying on a different one when a page fails:

import puppeteer from "puppeteer";
import { anonymizeProxy, closeAnonymizedProxy } from "proxy-chain";

const POOL = [
  "http://user:pass@203.0.113.7:8080",
  "http://user:pass@203.0.113.24:3128",
  "http://user:pass@198.51.100.14:8080",
];

async function scrape(url, tries = 3) {
  for (let i = 0; i < tries; i++) {
    const upstream = POOL[Math.floor(Math.random() * POOL.length)];
    const local = await anonymizeProxy(upstream);
    const browser = await puppeteer.launch({ args: [`--proxy-server=${local}`] });
    try {
      const page = await browser.newPage();
      await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
      return await page.content();
    } catch (err) {
      // dead or blocked exit, fall through to the next proxy
    } finally {
      await browser.close();
      await closeAnonymizedProxy(local, true).catch(() => {});
    }
  }
  throw new Error(`all ${tries} proxies failed for ${url}`);
}

The load-bearing parts are the same as any rotation loop: a fresh exit per attempt, a real navigation timeout so a silent proxy cannot hang the page, and always closing the browser and the local proxy in a finally so a failed attempt does not leak a Chromium process. You do not have to maintain the list by hand; our free proxy API returns a fresh pool you can load at startup and split into POOL.

Verify the exit IP

Never assume the flag took effect. Load an IP echo and confirm the address is the proxy's, not yours:

const page = await browser.newPage();
await page.goto("https://httpbin.org/ip");
const seen = JSON.parse(await page.evaluate(() => document.body.innerText)).origin;
console.log("exit IP:", seen);

If that address is your own, the proxy did not apply, which for an authenticated proxy almost always means the credentials were in the URL instead of in page.authenticate or proxy-chain.

Where to go from here

Puppeteer's proxy story is one flag and one gotcha. Get --proxy-server and the authentication trap right and the rest is the discipline every headless scraper needs: verify each exit, bound your concurrency, and rotate a pool that is actually alive. A real browser also leaks more than an IP, so pair the proxy with the fingerprint and behaviour hygiene in how to avoid IP bans while scraping, and note that a datacenter IP behind a real browser is still a datacenter IP to any site that checks.

For verifying exits, our proxy checker reports exit IP, anonymity grade, geolocation and latency in one paste. For the pool, the free proxy API hands you recently checked endpoints with no key, ideal for testing this code before a paid gateway. The Playwright guide is the sibling reference if you run the other headless library, and proxies for Puppeteer covers which proxy type each job needs. When launching a browser per proxy becomes the bottleneck, a rotating gateway that returns a fresh residential IP per request at $0.44/GB lets one browser reuse a single endpoint while the exit still changes underneath it.

Frequently asked questions

How do I set a proxy in Puppeteer?
Pass it as a launch argument: puppeteer.launch({ args: ['--proxy-server=http://host:port'] }). That routes the whole browser through the proxy. The catch is that Chromium's --proxy-server flag does not accept a username and password in the URL, so an authenticated proxy needs page.authenticate or a local forwarder like proxy-chain.
How do I use an authenticated proxy with Puppeteer?
Two ways. The simple one is page.authenticate({ username, password }) called before you navigate, which answers the proxy's 407 challenge. It works but applies per page and can be awkward with many tabs. The robust one is the proxy-chain package, which spins up a local proxy that holds the credentials and forwards to your upstream, so Chromium only ever sees an unauthenticated local address.
Can I set a different proxy per page in Puppeteer?
Not per page from one browser, because --proxy-server is set once at launch for the whole Chromium process. For a different exit per page, use one browser context per proxy with browser.createBrowserContext and a per-context proxy where supported, or the common pattern of launching a separate browser per proxy. proxy-chain also lets you change the upstream without relaunching Chromium.
How do I rotate proxies in Puppeteer?
Give each browser instance its own proxy from a pool and reuse it for a batch of pages, then relaunch with the next exit, because the proxy is fixed at launch. Retry on a different proxy when a page fails, since on any pool some exits are always down. proxy-chain reduces the relaunch cost by letting you swap the upstream on a running local proxy.
Why is my Puppeteer proxy not working?
The three usual causes are an authenticated proxy passed as a URL (Chromium ignores the credentials, so add page.authenticate or proxy-chain), a dead free proxy (verify it first), or a proxy set on only some pages. Confirm the exit by loading an IP echo service and checking the address is the proxy's, not yours.

Get proxies that are alive right now

Our free list re-checks every exit every few minutes and shows a last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup