🎉 Premium Proxies · 3-Day Free TrialClaim Now →
Guides

How to Use Proxies with Puppeteer

Use proxies with Puppeteer: set --proxy-server launch args, per-context proxies, page.authenticate for auth, rotation, stealth tips, and error fixes.

S SparkProxy 2 14 min read
Share
How to Use Proxies with Puppeteer

Setting up proxies with Puppeteer has one trap that breaks most first attempts: Chrome silently ignores any username and password you put in the --proxy-server flag. Pass --proxy-server=user:pass@host:port and the credentials vanish, so the browser hits a 407 loop and nothing loads. This guide covers every working method: launch args, page.authenticate() for credentials, per-context proxies that run several IPs in one browser process, rotation, SOCKS5, a full error reference, stealth tuning, and when to skip the whole setup and call the SparkProxy Scraping API instead.

Why Route Puppeteer Through a Proxy?

A proxy in Puppeteer routes the entire browser's network stack through another IP: page loads, XHR and fetch calls, images, fonts, WebSocket upgrades, everything Chromium requests. This differs from an HTTP-library proxy in Node's axios or undici, which only reroutes the calls your own code makes. When a page's JavaScript fires background API requests, those go through the browser proxy too, which is exactly what you want for scraping dynamic sites.

Use caseWhy the proxy matters
Scraping geo-restricted contentPresent a specific country's exit IP so the site serves local data
Price and inventory monitoringSpread requests across IPs to avoid single-IP rate limiting
Ad verificationRender ads from the target region's IP to see what real users see
Multi-account automationBind a stable IP per account to reduce association
SERP and marketplace scrapingRotate IPs to keep collecting past soft blocks

Puppeteer 22+ ships with a bundled Chrome for Testing build, so the examples below assume puppeteer (not puppeteer-core) on Node 18 or newer.


Set a Proxy with Launch Args

The base method passes --proxy-server in the args array at launch. This applies to every page the browser opens. It works cleanly for proxies that use IP whitelisting, where your server's public IP is authorized in the SparkProxy dashboard so no credentials are needed.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: true,
  args: [
    '--proxy-server=dc.sparkproxy.io:10000',
    '--no-sandbox',
    '--disable-dev-shm-usage',
  ],
});

const page = await browser.newPage();
await page.goto('https://httpbin.org/ip', { waitUntil: 'networkidle2' });
console.log(await page.evaluate(() => document.body.innerText));

await browser.close();

The value after --proxy-server= is host:port. Omit the scheme for a standard HTTP proxy and Chrome uses HTTP CONNECT tunneling by default. To skip the proxy for specific hosts, add a second arg like --proxy-bypass-list=localhost;127.0.0.1;*.internal.sparkproxy.io. Note the separator: Chrome's --proxy-bypass-list uses semicolons, not commas, which is a common copy-paste bug.


Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Proxy Authentication with page.authenticate()

Most datacenter and residential plans use username and password auth. Here is the rule that trips people up:

Chrome does not accept credentials in the --proxy-server flag.

// THIS DOES NOT WORK, credentials are silently dropped
args: ['--proxy-server=user:pass@dc.sparkproxy.io:10000'];

The credentials never reach the proxy, so the target returns a 407 challenge that Puppeteer cannot answer, and navigation hangs or throws net::ERR_INVALID_AUTH_CREDENTIALS. The fix is page.authenticate(), which registers credentials over the DevTools protocol and answers the proxy's auth challenge for you.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: true,
  args: ['--proxy-server=dc.sparkproxy.io:10000', '--no-sandbox'],
});

const page = await browser.newPage();

// Must run BEFORE the first navigation on this page
await page.authenticate({
  username: 'YOUR_PROXY_USER',
  password: 'YOUR_PROXY_PASS',
});

await page.goto('https://httpbin.org/ip', { waitUntil: 'domcontentloaded' });
console.log(await page.evaluate(() => document.body.innerText));

await browser.close();

Two details decide whether this works. First, call page.authenticate() before page.goto(), never after. Second, credentials are scoped to the page, so every new page you open needs its own authenticate() call. If you open ten tabs, you authenticate ten times.

For a deeper look at how the 407 handshake works under the hood, see the SparkProxy explainer on how proxy authentication works.


Per-Context Proxies in One Browser

Here is the technique most Puppeteer proxy guides still miss. Since Puppeteer 22.6, browser.createBrowserContext() accepts a proxyServer option, so you can run several different proxies inside a single browser process without relaunching Chrome. Each browser context is an isolated cookie and cache jar, and now it can carry its own exit IP.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });

// Context A on one proxy
const ctxA = await browser.createBrowserContext({
  proxyServer: 'http://dc.sparkproxy.io:10000',
  proxyBypassList: ['localhost', '127.0.0.1'],
});

// Context B on a different proxy, same browser process
const ctxB = await browser.createBrowserContext({
  proxyServer: 'http://dc.sparkproxy.io:10001',
});

const pageA = await ctxA.newPage();
await pageA.authenticate({ username: 'USER_A', password: 'PASS_A' });
await pageA.goto('https://httpbin.org/ip');

const pageB = await ctxB.newPage();
await pageB.authenticate({ username: 'USER_B', password: 'PASS_B' });
await pageB.goto('https://httpbin.org/ip');

console.log('A:', await pageA.evaluate(() => document.body.innerText));
console.log('B:', await pageB.evaluate(() => document.body.innerText));

await browser.close();

createBrowserContext() replaced the deprecated createIncognitoBrowserContext() in Puppeteer 22, so update older snippets that still call the incognito method. The proxyServer takes a full URL string, and proxyBypassList takes an array of host patterns. Credentials still go through page.authenticate() on each page inside the context, because auth is a page-level concern even when the proxy is set at the context level.

This matters for throughput. Launching a fresh Chrome per proxy costs 200 to 400 MB of RAM and a cold start each time. Twenty proxies means twenty browsers. With per-context proxies, one browser process holds twenty isolated contexts, each on its own IP, and you skip the relaunch cost entirely.


Rotating Proxies Across Pages and Contexts

There are two rotation models, and the right one depends on your Puppeteer version and workload.

Rotate by browser process (any version)

If you are on an older Puppeteer, or you want full process isolation per IP, launch a new browser per proxy. It is heavier but simple and version-agnostic.

async function scrapeWithNewBrowser(url, proxy) {
  const browser = await puppeteer.launch({
    headless: true,
    args: [`--proxy-server=${proxy.server}`, '--no-sandbox'],
  });
  const page = await browser.newPage();
  try {
    await page.authenticate({ username: proxy.user, password: proxy.pass });
    await page.goto(url, { waitUntil: 'domcontentloaded' });
    return await page.content();
  } finally {
    await browser.close();
  }
}

The rotation logic (round-robin, weighted, sticky per account, retry on failure) is the same theory whatever the language. For the algorithm design side, including per-IP request budgeting, see how to rotate proxies in Python, which maps cleanly onto Node.


SOCKS5 and Other Proxy Schemes

Chrome supports HTTP, HTTPS, and SOCKS5 proxies through the same --proxy-server flag. Prefix the value with the scheme:

// SOCKS5 via launch args (IP-whitelisted, no in-flag auth)
const browser = await puppeteer.launch({
  args: ['--proxy-server=socks5://dc.sparkproxy.io:1080', '--no-sandbox'],
});

The same socks5:// URL works as the proxyServer value in createBrowserContext() too. One caveat specific to SOCKS: Chrome does not support username and password authentication for SOCKS5 proxies through page.authenticate(). If your SOCKS5 endpoint needs credentials, use IP whitelisting instead, or route through an HTTP proxy where page.authenticate() works.

By default, Chrome resolves DNS locally before connecting through SOCKS5. If you need the proxy exit to resolve DNS (to prevent DNS leaks and match the exit geo), enforce it with a --host-resolver-rules arg rather than relying on the browser's local resolver. Most datacenter scraping runs fine on plain HTTP proxies, so reach for SOCKS5 only when a target or protocol specifically needs it.


Handling Proxy Errors

Proxy failures in Puppeteer surface as net::ERR_* strings thrown from page.goto(). Wrap navigation, inspect the message, and rotate or retry.

async function safeGoto(page, url, proxy) {
  try {
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
    return true;
  } catch (err) {
    const msg = err.message;
    if (msg.includes('ERR_TUNNEL_CONNECTION_FAILED') ||
        msg.includes('ERR_PROXY_CONNECTION_FAILED')) {
      console.warn(`Proxy ${proxy.server} unreachable, rotating.`);
      return false; // caller picks the next proxy
    }
    if (msg.includes('ERR_INVALID_AUTH_CREDENTIALS')) {
      console.error('Bad proxy credentials or authenticate() ran after goto.');
      throw err;
    }
    throw err; // real page error, surface it
  }
}

The common ones and their fixes:

Error stringCauseFix
`net::ERR_INVALID_AUTH_CREDENTIALS`Credentials in `--proxy-server`, or `authenticate()` called after `goto()`Move `page.authenticate()` before the first `goto()`; never put user:pass in the flag
`net::ERR_TUNNEL_CONNECTION_FAILED`Proxy host unreachable or port blockedTest `curl -x host:port https://httpbin.org/ip`; confirm the port is open
`net::ERR_PROXY_CONNECTION_FAILED`Proxy down or wrong hostRotate to a healthy proxy; verify the endpoint
`net::ERR_NO_SUPPORTED_PROXIES`Wrong scheme (for example `https://` for an HTTP proxy)Drop the scheme or use `http://` / `socks5://` correctly
`net::ERR_EMPTY_RESPONSE`Proxy dropped the connection mid-responseRetry; if it repeats, the IP is likely blocked, rotate it
Navigation hangs, no error407 loop from missing authAdd `page.authenticate()` before navigation

If pages load but the target keeps serving CAPTCHAs or blocks, the problem is detection, not connectivity. The playbook for that (fingerprint hygiene, request pacing, IP quality) is covered in how to avoid getting your proxy blocked.


Stealth Tips for Proxied Puppeteer

A clean IP does not help if the browser broadcasts that it is automated. Pair your proxy with these adjustments.

Use puppeteer-extra with the stealth plugin to patch the most common automation tells (navigator.webdriver, headless Chrome markers, WebGL vendor strings):

import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';

puppeteer.use(StealthPlugin());

const browser = await puppeteer.launch({
  headless: true, // default "new" headless in Puppeteer 22+
  args: ['--proxy-server=dc.sparkproxy.io:10000', '--no-sandbox'],
});

Stick with the default new headless mode (headless: true in Puppeteer 22+). The legacy chrome-headless-shell (set via headless: 'shell') exposes more detectable signals and should only be used when you need its lower footprint.

Match the browser's declared identity to the proxy's exit country. A German exit IP with an en-US locale and a New York timezone is an obvious mismatch:

const page = await browser.newPage();
await page.authenticate({ username: 'USER', password: 'PASS' });

// Align locale, timezone, and headers with the proxy geo (example: Germany)
await page.setExtraHTTPHeaders({ 'Accept-Language': 'de-DE,de;q=0.9' });
await page.emulateTimezone('Europe/Berlin');
await page.setUserAgent(
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
  '(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
);

await page.goto('https://httpbin.org/headers');

Two more habits pay off. Add human-like pacing with small randomized delays between actions instead of firing navigations back to back. And prefer residential exits for the hardest targets, since datacenter subnets are easier to flag. The tradeoffs between datacenter, residential, and browser-automation proxies are laid out in the SparkProxy guide on integrating proxies with Selenium, which shares the same underlying Chrome behavior as Puppeteer.


Offloading to the SparkProxy Scraping API

Running Puppeteer plus a proxy pool plus stealth patches plus retry logic is a lot of moving parts to maintain. When you would rather send a URL and get back rendered HTML, the SparkProxy Scraping API handles the headless browser, proxy rotation, and anti-bot layers on the server side. It is documented at the SparkProxy Scraping API reference.

The endpoint is https://scrape.sparkproxy.io/api/v1, authenticated with an X-API-Key header. A minimal call from Node using the built-in fetch:

const params = new URLSearchParams({
  url: 'https://www.sparkproxy.io',
  render_js: 'true',       // headless Chromium rendering
  premium_proxy: 'true',   // residential exit
  country_code: 'US',      // ISO 3166-1 alpha-2
  format: 'html',
});

const res = await fetch(`https://scrape.sparkproxy.io/api/v1?${params}`, {
  headers: { 'X-API-Key': 'sk-YOUR_API_KEY' },
});

console.log('Credits used:', res.headers.get('X-Credits-Used'));
const html = await res.text();
console.log(html.slice(0, 200));

If you still need browser interactions (click, scroll, fill, wait for a selector), you can describe them with js_scenario and let the API drive the page, so you keep Puppeteer-style control without managing the browser or the proxies yourself:

const body = {
  url: 'https://www.sparkproxy.io/pricing',
  render_js: true,
  premium_proxy: true,
  wait_for: '#pricing-table',        // wait for a selector (max 30s)
  js_scenario: {
    steps: [
      { click: '#load-more' },
      { wait: 2 },
    ],
  },
  format: 'md',                       // return clean Markdown
};

const res = await fetch('https://scrape.sparkproxy.io/api/v1', {
  method: 'POST',
  headers: { 'X-API-Key': 'sk-YOUR_API_KEY', 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
});

const data = await res.text();

Use self-managed Puppeteer when you need deep, stateful control of a real browser you own. Use the Scraping API when the goal is reliable extraction at scale and you would rather not babysit browsers and IPs.


Frequently asked questions

FAQ

No. Chrome silently drops credentials passed in --proxy-server, which causes a 407 loop or ERR_INVALID_AUTH_CREDENTIALS. Call page.authenticate({ username, password }) before the first page.goto() instead, which answers the proxy's authentication challenge over the DevTools protocol.

Not per individual page, but per browser context, which is close. Create a context with browser.createBrowserContext({ proxyServer }) (Puppeteer 22.6+) and open pages inside it. Every page in that context shares the context's proxy, and different contexts can use different proxies in the same browser process.

Use per-context proxies. Keep one long-lived browser and create a fresh context bound to the next proxy in your pool for each job, then call context.close() when the job finishes. This avoids the 200 to 400 MB and cold-start cost of launching a new Chrome per proxy.

The two usual causes are calling it after page.goto() instead of before, or calling it on a different page than the one that navigates. Credentials are scoped to a single page, so run page.authenticate() on each new page before its first navigation.

Use the default new headless mode (headless: true in Puppeteer 22+). The legacy chrome-headless-shell (headless: 'shell') exposes more automation signals and is easier for anti-bot systems to flag. Combine the new headless mode with puppeteer-extra-plugin-stealth for the best results.

Datacenter proxies work well for many targets and are faster and cheaper, so start there. Switch to residential exits (the premium_proxy option on the SparkProxy Scraping API) for sites with aggressive anti-bot detection, where datacenter subnets get flagged quickly.


Limited-time · 50% off

Get 50% off your first purchase

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Offer ends soon — claim it before it's gone

Claim Discount

About the Author

SparkProxy Technical Team, The SparkProxy engineering team builds and maintains global datacenter and residential proxy infrastructure, plus the SparkProxy Scraping API. This guide reflects proxy integration patterns tested with Puppeteer 22 and 23, Chrome for Testing 124+, and Node 18 and 20. SparkProxy's products include datacenter proxies, residential proxies, and a rendering Scraping API for large-scale data collection.

Citations: Puppeteer API: Browser.createBrowserContext · Puppeteer API: Page.authenticate · SparkProxy Scraping API Reference

Keep reading

Related articles

How to Rotate Proxies in Node.js

How to Rotate Proxies in Node.js

Rotate proxies in Node.js with round-robin, random, weighted, and sticky strategies, plus working axios, got, and node-fetch code and a reusable pool class.

SparkProxy·Guides