Save 5% every month: use code 5OFFSTORM at checkout
Home / Guides / Puppeteer

How to set up a proxy in Puppeteer

Launch Chrome with args: ['--proxy-server=http://GATEWAY_IP:PORT']. Every page in that browser then goes through the gateway. If the proxy needs a username and password, call page.authenticate() before page.goto(); with an IP-authorized gateway you skip that step entirely.

Updated October 2026Code & scraping frameworks

Puppeteer is Google’s Node.js library for controlling Chrome (and Firefox) over the DevTools and WebDriver BiDi protocols. It’s the go-to tool for screenshots, PDFs, scraping single-page apps and automating flows that need a real browser. It has no proxy option of its own at launch. Instead you pass Chrome’s --proxy-server command-line flag, and Chrome does the proxying.

That design causes most of the confusion. Chrome ignores a username and password written into the flag, so the obvious http://user:pass@host:port fails. Many tutorials then reach for plugins and helper packages you don’t need. Others still claim Puppeteer can only use one proxy per browser, which hasn’t been true since browser contexts gained their own proxyServer option.

This guide covers the launch flag, authentication when a proxy needs it, separate proxies per browser context, how many tabs your plan can carry, and the Chrome network errors you’ll actually run into.

Which Storm plan fits

Most Puppeteer scrapers fit a rotating proxy plan: the launch flag points at one gateway and the pool does the rotating. Size it for tabs, about 10 threads each, so 40 threads is 4 pages in parallel; the 10-thread package is for a quick test. For one fixed IP per account, use a residential port or a dedicated proxy per browser context.

Rotating proxies (from $14/mo): 700,000+ IPs behind fixed gateway IP:PORTs. New IP on every request, or every 3 or 15 minutes. USA, EU, USA+EU or Worldwide. Unlimited bandwidth on every plan.

Get 40 threads for $39/mo See all rotating proxies plans

Before you start

  1. Log in to the member area and copy your gateway IP:PORTs. They never change; the rotation happens on our side.
  2. Add the public IP of the computer or server that will run your tool under Authorized IPs, click Save, and allow up to 15 minutes before testing. Rotating and residential proxies use IP authentication, so there is no username or password.
  3. Dedicated proxies work with either IP authentication or a username and password. Use user:pass if your IP changes or the tool runs on several machines.
  4. Count your threads: the tool’s total open connections must stay within your plan (for example 40 threads on the 40-thread plan).

Set up a Puppeteer proxy, step by step

  1. Install Puppeteer

    Run npm i puppeteer. It downloads a compatible Chrome build on install, so the flag behaviour below matches the browser it launches. Use puppeteer-core only when you point it at a Chrome you installed yourself.

  2. Add the --proxy-server flag

    Pass args: ['--proxy-server=http://GATEWAY_IP:PORT'] to puppeteer.launch(). Write only scheme, host and port. Credentials in this string are not supported by Chrome and lead to errors.

  3. Authenticate only if the proxy asks

    For rotating and residential gateways, authorize your machine’s IP in the member area and skip this step. For a dedicated proxy with user:pass, call await page.authenticate({ username, password }) on each new page before navigating.

  4. Check the exit IP

    Navigate to https://httpbin.org/ip and read document.body.innerText. You should see a proxy IP. If the page shows your own address, the browser wasn’t launched with the flag (a reused, already-running browser is a common cause).

  5. Give contexts their own proxy when needed

    To run several identities in one browser, call browser.createBrowserContext({ proxyServer: 'http://GATEWAY_IP:PORT' }) for each. Each context has its own cookies, cache and gateway.

  6. Cap open pages and raise timeouts

    Keep open pages at roughly your plan’s threads divided by 10, close each page when done, and set page.setDefaultNavigationTimeout(60000). The default is 30 seconds, which heavy pages through a proxy can miss.

Puppeteer proxy code

Put a gateway from your member area in place of GATEWAY_IP:PORT. With rotating and residential plans the gateway recognises your authorized IP, so there’s no authenticate() call in those examples.

Basic: whole browser through one gateway
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  args: ['--proxy-server=http://GATEWAY_IP:PORT'],
});
const page = await browser.newPage();
await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded', timeout: 60000 });
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
Dedicated proxy with username and password
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  args: ['--proxy-server=http://PROXY_IP:PORT'],   // no user:pass here
});
const page = await browser.newPage();
await page.authenticate({ username: 'USERNAME', password: 'PASSWORD' });  // before goto
await page.goto('https://httpbin.org/ip');
console.log(await page.evaluate(() => document.body.innerText));
await browser.close();
A different gateway per browser context
import puppeteer from 'puppeteer';

const gateways = [
  'http://GATEWAY_IP:PORT1',   // e.g. residential port 1
  'http://GATEWAY_IP:PORT2',   // residential port 2
  'http://GATEWAY_IP:PORT3',   // residential port 3
];

const browser = await puppeteer.launch();
await Promise.all(gateways.map(async (proxyServer, i) => {
  const ctx = await browser.createBrowserContext({ proxyServer });
  const page = await ctx.newPage();
  await page.goto('https://httpbin.org/ip', { timeout: 60000 });
  console.log(i, await page.evaluate(() => document.body.innerText));
  await ctx.close();
}));
await browser.close();
Worker pool sized to a 40-thread plan, images blocked
import puppeteer from 'puppeteer';

const MAX_PAGES = 4;                                   // 40 threads / ~10 per page
const urls = Array.from({ length: 30 }, (_, i) => `https://httpbin.org/anything/${i}`);

const browser = await puppeteer.launch({ args: ['--proxy-server=http://GATEWAY_IP:PORT'] });

async function worker() {
  while (urls.length) {
    const url = urls.shift();
    const page = await browser.newPage();
    page.setDefaultNavigationTimeout(60000);
    await page.setRequestInterception(true);
    page.on('request', req =>
      ['image', 'media', 'font'].includes(req.resourceType()) ? req.abort() : req.continue());
    try {
      const res = await page.goto(url, { waitUntil: 'domcontentloaded' });
      console.log(url, res?.status());
    } catch (err) {
      console.log(url, err.message);
    } finally {
      await page.close();
    }
  }
}

await Promise.all(Array.from({ length: MAX_PAGES }, worker));
await browser.close();
Skip the proxy for local hosts
const browser = await puppeteer.launch({
  args: [
    '--proxy-server=http://GATEWAY_IP:PORT',
    '--proxy-bypass-list=localhost;127.0.0.1',     // only works together with --proxy-server
  ],
});

Why credentials don’t go in --proxy-server

--proxy-server is a Chrome switch, and its syntax only covers a scheme, a host and a port (optionally per URL scheme, separated by semicolons). There’s no slot for a username or password. Put user:pass@ in it and Chrome rejects the proxy, typically with ERR_NO_SUPPORTED_PROXIES, or it answers the proxy’s 407 challenge with nothing and the navigation fails.

Puppeteer’s fix is page.authenticate(). It answers the proxy’s authentication challenge for that page. Two details matter. It must run before the first goto() on that page, and it must run on every new page, because it’s a per-page setting. And according to the Puppeteer docs, it turns on request interception behind the scenes, which can slow pages down.

IP authorization avoids all of that. On Storm rotating and residential plans it’s the only method: you save your server’s IP in the member area and the gateway lets it in. Dedicated proxies support it too, so even there you can drop authenticate() when Puppeteer always runs from the same IP.

One proxy per browser, per context, or per page

Per browser is the launch flag. Every context and page shares it. Launching a second browser for a second proxy works but costs a full Chrome process each time.

Per context is createBrowserContext({ proxyServer }), with an optional proxyBypassList. Contexts are isolated: separate cookies, storage and cache. That makes them the right unit for one account per IP: give each context its own residential port or dedicated proxy and keep them apart for the whole session. On a dedicated proxy with user:pass, call authenticate() on every page in that context.

Per page isn’t something Puppeteer offers. Packages that fake it by intercepting each request and re-sending it from Node.js change how the traffic looks, because the requests no longer come from Chrome’s own network stack. In most cases a context per proxy does the same job more cleanly. Helper libraries that start a local forwarding proxy also exist, but with IP-authorized gateways you don’t need one. And you don’t need puppeteer-extra or a stealth plugin to use a proxy; plain Puppeteer handles it.

How many tabs your plan can carry

Every tab is a small web browser on its own: the page, its scripts, styles, fonts, images and API calls all open connections, several in parallel per host. Count about 10 threads per open page.

  • Rotating: 10 threads = 1 tab, 40 = 4, 80 = 8, 150 = 15, 200 = 20.
  • Residential: up to 50 threads per port, so around 5 tabs per port, all on that port’s current IP.
  • Dedicated: up to 100 threads per proxy, so around 10 tabs on one static IP.

Two habits stretch a plan further. Abort images, media and fonts with request interception when you only need text. And always close pages in a finally block; a tab left open after an error keeps its connections and slowly eats your thread budget until new pages start failing.

Rotation and sessions in a Puppeteer run

The Main rotating gateway picks a new IP for each new connection. Chrome keeps connections alive and reuses them, so one tab tends to stay on one IP while it loads, and later tabs to the same host may reuse those connections too. For a fresh IP per job, give each job a new browser context and close it when finished; a closed context drops its connections.

For flows that must keep an IP, such as a login followed by a dashboard, use the 15-minute gateway or a residential port and keep the flow inside one context. Residential IPs switch at minute 1, 6, 11, 16 and so on of each hour, and a load that spans the switch can fail, so wrap goto() in a short retry.

Running Puppeteer on servers, Docker and serverless

In Docker on a VPS, the container’s outgoing IP is usually the host’s public IP: authorize that one. On serverless platforms the outgoing IP can change between invocations, so an IP-authorized gateway won’t accept the connection reliably. Move the browser to a fixed-IP server, or use private dedicated proxies with authenticate(). If your home IP changes, authorize a dynamic DNS hostname instead of the raw IP.

Common errors and fixes

net::ERR_NO_SUPPORTED_PROXIESThe --proxy-server value has credentials or a malformed scheme. Use --proxy-server=http://IP:PORT only, and move user:pass to page.authenticate().
net::ERR_PROXY_CONNECTION_FAILEDChrome can’t reach the gateway at all: a wrong IP or port, or outbound traffic blocked by a firewall. See the connection-failed guide.
net::ERR_TUNNEL_CONNECTION_FAILEDThe HTTPS tunnel wasn’t opened: your IP isn’t authorized yet, so the gateway resets the connection (allow 15 minutes after saving), or too many tabs are open for your plan. See this guide.
407 Proxy Authentication Required / ERR_HTTP_RESPONSE_CODE_FAILUREOn a dedicated proxy, authenticate() was missing, ran after goto(), or has the wrong credentials. On rotating or residential an unauthorized IP shows as a connection or tunnel error, not a 407, so a 407 there comes from another proxy in the chain. See the 407 guide.
TimeoutError: Navigation timeout of 30000 ms exceededThe page didn’t finish loading in 30 seconds. Raise the timeout to 60 seconds, use waitUntil: 'domcontentloaded', and lower the number of open tabs.
Real IP shows despite the flagPuppeteer connected to an existing browser (puppeteer.connect()) that was started without the flag, or the flag has a typo. Launch a fresh browser with the flag and check again.

FAQ

Do Storm rotating proxies need page.authenticate() in Puppeteer?

No. Rotating and residential plans use IP authorization only, so --proxy-server=http://GATEWAY_IP:PORT is the whole setup. You only call authenticate() for private dedicated proxies that use username and password.

How many Puppeteer pages can I open on a 40-thread plan?

About 4 at the same time, counting roughly 10 threads per page. Blocking images and fonts helps; heavy single-page apps need more headroom. Scale up to 80, 150 or 200 threads for more parallel tabs.

Can I use a different proxy for each page in Puppeteer?

Not natively. Use one browser context per proxy with createBrowserContext({ proxyServer }) and open the pages for that proxy inside it.

Do I need puppeteer-extra to use a proxy?

No. Proxies work with plain Puppeteer through the launch flag or context options. Add plugins only for features you actually need.

How do I rotate proxies in Puppeteer?

Point the browser at a rotating gateway and let the gateway change the IP. For a fresh IP per job, open a new browser context per job. There’s no need to restart Chrome with a new flag from a list.

Can I use a SOCKS5 proxy in Puppeteer?

Chrome accepts socks5:// in the flag, but Storm gateways aren’t SOCKS proxies. Use the http:// scheme; it carries https sites through a CONNECT tunnel.

Still have questions? Contact us here. A real person answers.

Related guides

Tool facts checked against the official documentation (October 2026): Puppeteer: Page.authenticate · Puppeteer: BrowserContextOptions (proxyServer) · Puppeteer: Browser.createBrowserContext · Puppeteer: WaitForOptions (timeout default) · Puppeteer: Headless modes · Chromium: network settings and proxy flags. Storm Proxies facts: our plans page and refund policy.

Unlimited bandwidth. One flat monthly price.

Access is live the moment you pay, and the smallest package of each proxy type has a 24-hour money-back guarantee on your first order.