Web Scraping & Automation

Puppeteer Proxies: The Complete Guide to Using Proxies With Puppeteer

How to configure proxies in Puppeteer: launch flags, authentication, rotating and per-page proxies, SOCKS5, stealth, bandwidth saving and the best proxy types for headless Chrome. Working code included.

By Team of Monkeys Editorial Updated 44 min read Affiliate disclosure

This guide contains affiliate links to Geonode. If you buy through them we may earn a commission, at no extra cost to you.

Why Puppeteer needs proxies

Puppeteer is Google's Node.js library for controlling Chrome and Chromium. It's one of the most popular tools for scraping JavaScript-heavy websites, automating workflows, generating screenshots and PDFs, and testing web apps. It's also very easy for websites to notice when Puppeteer is making a lot of requests from the same IP address.

Proxies solve the IP side of that problem. With the right proxy setup, each browser (or each page) can appear to come from a different location, you can access geo-restricted content, and you can spread load across many IPs so that rate limits don't stop your jobs.

This guide covers everything you need: how Chrome's proxy settings work inside Puppeteer, how to authenticate, how to rotate IPs, how to use a different proxy per page, which proxy types work best, and how to keep bandwidth costs under control. Every example uses current Puppeteer syntax.

What teams use Puppeteer and proxies for

Before the code, it's useful to see the range of jobs this combination handles, because the right proxy setup depends on the job.

  • Scraping JavaScript-heavy sites. Single-page apps, infinite scroll and data loaded after the page renders all need a real browser. Proxies spread the load and get past IP-based blocks. Rotating residential proxies with short sticky sessions are the usual choice.
  • Monitoring prices, stock and content. Scheduled jobs check pages for changes. If the data is in plain HTML, an HTTP client is cheaper; Puppeteer earns its place when scripts generate the content.
  • Visual checks from different locations. Screenshots and PDFs of pages as they appear in different countries, for ad verification, compliance archives or localisation QA. Images must load, so bandwidth matters.
  • SEO and performance audits. Rendering pages the way search engines and users in specific regions see them, measuring load times from different locations, and checking geo-redirects.
  • End-to-end testing of geo features. Confirming that your own app shows the right language, currency, prices and legal text in each market. Datacenter proxies often suffice, since your own site won't block them.
  • Workflow automation. Logging into dashboards, downloading reports and filling in forms, often with a static IP that the target has whitelisted.
  • Security research. Visiting suspicious pages from realistic locations in an isolated browser, to see what victims see.

Each job pulls the proxy choice in a different direction: diversity for scraping, location accuracy for visual checks, a permanent IP for workflow automation. Keep the job in mind as you read the rest of this guide.

Headless modes and why they matter for proxies

Chrome has had two headless implementations. The older one was a separate, lighter browser with noticeable differences from normal Chrome. The newer headless mode runs the real Chrome browser without a visible window, and recent Puppeteer versions use it by default.

For proxy work, the practical effects are:

  • Fewer detectable differences between headless and headful Chrome, so good proxies are less likely to be undermined by an obviously headless browser.
  • The same network stack as normal Chrome, so proxy flags, authentication and per-context proxies behave identically in both modes.
  • Slightly higher resource use than the old lightweight mode.

Some teams still run headful Chrome under a virtual display for the very hardest targets. Proxy configuration is the same either way.

The basics: launching Puppeteer with a proxy

Chrome accepts a proxy through the --proxy-server command-line flag. In Puppeteer, you pass it in args when you launch the browser:

javascript
import puppeteer from 'puppeteer';

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

A few points to note:

  • The flag applies to the whole browser. Every page and tab uses the same proxy.
  • Chrome supports http://, https://, socks4:// and socks5:// schemes in the flag.
  • You can't put a username and password in the flag. Chrome ignores credentials in --proxy-server. Use one of the authentication methods below.

Authenticating proxies in Puppeteer

Almost every paid proxy requires authentication. You have three options.

Option 1: page.authenticate()

This is the standard approach for HTTP proxies:

javascript
const browser = await puppeteer.launch({
  args: ['--proxy-server=http://gate.example-provider.com:7777'],
});
const page = await browser.newPage();
await page.authenticate({ username: 'USER', password: 'PASS' });
await page.goto('https://example.com');

You must call page.authenticate() on every new page before navigating. Puppeteer answers Chrome's proxy authentication challenge for you.

Option 2: IP whitelisting

Most providers let you whitelist your server's IP address in their dashboard. Requests from that IP are accepted without credentials, so the launch flag alone is enough. This is the cleanest option for servers with a fixed IP, and it works with SOCKS5, which page.authenticate() does not. See proxy authentication methods.

Option 3: a local forwarding proxy

Libraries such as proxy-chain start a local, unauthenticated proxy that forwards to your authenticated upstream proxy:

javascript
import puppeteer from 'puppeteer';
import proxyChain from 'proxy-chain';

const upstream = 'http://USER:PASS@gate.example-provider.com:7777';
const localUrl = await proxyChain.anonymizeProxy(upstream);

const browser = await puppeteer.launch({ args: [`--proxy-server=${localUrl}`] });
// ... use the browser ...
await browser.close();
await proxyChain.closeAnonymizedProxy(localUrl, true);

This is handy for SOCKS5 with credentials and for tools that don't expose page.authenticate(). Full details: Puppeteer proxy authentication.

Rotating proxies with Puppeteer

There are two ways to rotate IPs.

Provider-side rotation. Use a rotating residential gateway. Each new connection or each new session ID gets a new IP. You keep the same --proxy-server, and change the session parameter in the username to switch IPs:

javascript
function sessionUser(base) {
  return `${base}-session-${Math.random().toString(36).slice(2, 10)}`;
}

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

Note that Chrome keeps connections alive and may reuse an existing connection, so a new session username is most reliable when combined with a new browser context or a new browser.

Client-side rotation. Keep a list of static proxies and launch a new browser per proxy, or per batch of jobs. This is common with ISP and datacenter proxies.

Read Puppeteer rotating proxies for complete patterns including retries and ban detection, and proxy rotation strategies for the theory.

Using a different proxy per page

Because --proxy-server is browser-wide, using different proxies for different pages takes extra work. Options:

  1. One browser per proxy. Simple and robust, but memory-hungry.
  2. Browser contexts with their own proxy. Recent Puppeteer versions support a proxyServer option on browser.createBrowserContext(), giving each incognito-style context its own proxy:
javascript
const browser = await puppeteer.launch();
const ctx = await browser.createBrowserContext({ proxyServer: 'http://gate.example-provider.com:7777' });
const page = await ctx.newPage();
await page.authenticate({ username: 'USER-session-1', password: 'PASS' });
await page.goto('https://example.com');
  1. Request interception with a proxying library. Packages such as puppeteer-page-proxy re-issue requests through a chosen proxy in Node. They work, but they bypass Chrome's network stack, which changes your TLS fingerprint and can increase detection.

The browser-context approach is the best balance today. See Puppeteer proxy per page.

SOCKS5 proxies in Puppeteer

javascript
const browser = await puppeteer.launch({
  args: ['--proxy-server=socks5://203.0.113.10:1080'],
});

Chrome doesn't support SOCKS5 authentication, so either whitelist your IP or use a local forwarder such as proxy-chain. Chrome resolves DNS through the SOCKS5 proxy by default, which is good for privacy. See Puppeteer SOCKS5 proxy.

How Chrome actually uses your proxy

Many confusing Puppeteer proxy problems make sense once you know what Chrome does under the hood.

HTTPS goes through a CONNECT tunnel

When Chrome loads an HTTPS page through an HTTP proxy, it sends a CONNECT example.com:443 request to the proxy, the proxy opens a TCP connection to the site, and Chrome negotiates TLS directly with the site inside that tunnel. The proxy sees the hostname but not the URL path, headers or content. That's why proxy errors on HTTPS sites appear as ERR_TUNNEL_CONNECTION_FAILED: the tunnel itself failed, before any page loaded.

DNS is resolved by the proxy

With an HTTP proxy, Chrome sends the hostname to the proxy in the CONNECT request, so the proxy resolves DNS, not your machine. With socks5://, Chrome also resolves hostnames through the proxy. With socks4://, DNS is resolved locally, which can leak the sites you visit to your local resolver. See DNS leaks and proxies.

Connections are reused

Chrome keeps connections to the proxy open and reuses them for later requests, and it multiplexes many requests over one HTTP/2 connection to a site. That's efficient, but it means changing your proxy username (for example, a new session ID) doesn't necessarily change your exit IP for requests that reuse an existing connection. The reliable way to force a new IP is a new browser context or a new browser, as in the production pattern below.

QUIC and HTTP/3 can bypass expectations

Chrome may try HTTP/3 (QUIC, over UDP) for sites that support it. Most HTTP proxies only carry TCP, so Chrome falls back to TCP through the proxy. If you see odd behaviour, or want to be certain all traffic goes through the proxy, disable QUIC:

javascript
const browser = await puppeteer.launch({
  args: ['--proxy-server=http://gate.example.com:7777', '--disable-quic'],
});

WebRTC can reveal your real IP

WebRTC uses UDP and can expose local and public IPs unless restricted. The --force-webrtc-ip-handling-policy=disable_non_proxied_udp flag in the production pattern below prevents that. See WebRTC leaks and proxies.

Some requests skip the proxy by default

Chrome bypasses the proxy for localhost and loopback addresses by default. You can change bypass rules with --proxy-bypass-list, or with the proxyBypassList option on a browser context:

javascript
const ctx = await browser.createBrowserContext({
  proxyServer: 'http://gate.example.com:7777',
  proxyBypassList: ['*.internal.example.com', 'localhost'],
});

Bypass lists are useful for sending your own API calls or internal services direct while the target site goes through the proxy.

Sending only some requests through the proxy

Sometimes you only want the target site's main requests to use the (paid) proxy, and you want third-party assets either blocked or loaded directly. Options:

  1. Block third parties with request interception (shown in the bandwidth section). This is the simplest and saves the most.
  2. Bypass specific hosts with proxyBypassList so they load directly from your server's IP. Be careful: this reveals your server's IP to those hosts, and some bot protection compares IPs across the page's requests. Only bypass hosts you control or hosts that don't matter for detection.
  3. Use a PAC file for complex routing rules. Chrome supports --proxy-pac-url. See proxy PAC files.

For most scraping, option 1 is the right answer. Option 2 is mainly for internal endpoints and tooling.

Using Geonode with Puppeteer

We're a Geonode affiliate, so here's how to plug its residential proxies into the patterns in this guide. The details below follow Geonode's dashboard conventions at the time of writing; the dashboard's endpoint generator always shows the exact values for your account.

Get your endpoint

In the Geonode dashboard, open the residential proxy configuration, choose rotating or sticky sessions and your target location, and generate an endpoint in host:port:username:password format. Geonode's documentation shows targeting added to the username with suffixes such as -country-jp, and rotating sessions on a range of ports. Put the values in environment variables:

bash
export GEONODE_HOST="<host from dashboard>"
export GEONODE_PORT="<port from dashboard>"
export GEONODE_USER="<username from dashboard>"
export GEONODE_PASS="<password from dashboard>"

Launch and authenticate

javascript
import puppeteer from 'puppeteer';

const { GEONODE_HOST, GEONODE_PORT, GEONODE_USER, GEONODE_PASS } = process.env;

const browser = await puppeteer.launch({
  args: ['--force-webrtc-ip-handling-policy=disable_non_proxied_udp', '--disable-quic'],
});

async function newGeonodePage(country) {
  const ctx = await browser.createBrowserContext({
    proxyServer: `http://${GEONODE_HOST}:${GEONODE_PORT}`,
  });
  const page = await ctx.newPage();
  const username = country ? `${GEONODE_USER}-country-${country}` : GEONODE_USER;
  await page.authenticate({ username, password: GEONODE_PASS });
  return { ctx, page };
}

const { ctx, page } = await newGeonodePage('us');
await page.goto('https://api.ipify.org');
console.log(await page.evaluate(() => document.body.innerText));
await ctx.close();

If the username format in your dashboard differs from this pattern, use the dashboard's version. Everything else in this guide (resource blocking, timezone matching, retries) applies unchanged.

Sticky sessions for logged-in flows

For flows that need one IP, generate a sticky endpoint in the dashboard (Geonode lists sticky sessions of up to 24 hours) and use one context per session. Geonode's dashboard also shows active sticky sessions, which helps when you're debugging which IP a browser is using.

Which Geonode product for which Puppeteer job

JobGeonode product
Scraping protected sites at scaleRotating residential
Logged-in sessions and checkoutsSticky residential, or ISP proxies for permanent IPs
Lenient sites, high volumeRotating datacenter
A few very hard targetsScraper API with JavaScript rendering instead of your own browser

For a full walkthrough, see Geonode with Puppeteer, and for the managed alternative, the Geonode Scraper API.

Which proxy type is best for Puppeteer?

Proxy typeFit for PuppeteerNotes
Rotating residential (sticky sessions)ExcellentBest default for protected sites; use sessions of 5 to 30 minutes per browser
ISP / static residentialExcellentFast, stable, great for logged-in workflows
DatacenterGood for tolerant sitesFast and cheap; blocked by serious bot protection
MobileExcellent but costlyUse only for the hardest targets

Headless browsers are bandwidth-hungry. A single page load can pull several megabytes of images, fonts, scripts and tracking pixels. On per-GB residential proxies that adds up quickly, which is why bandwidth control (next section) matters so much. If cost is the main concern, see cheap proxies and consider ISP proxies, which are priced per IP rather than per GB.

Cutting bandwidth: block what you don't need

Request interception lets you abort images, media, fonts and third-party trackers:

javascript
await page.setRequestInterception(true);
const BLOCK_TYPES = new Set(['image', 'media', 'font']);
const BLOCK_HOSTS = ['google-analytics.com', 'doubleclick.net', 'facebook.net'];

page.on('request', (req) => {
  const url = req.url();
  if (BLOCK_TYPES.has(req.resourceType()) || BLOCK_HOSTS.some((h) => url.includes(h))) {
    return req.abort();
  }
  req.continue();
});

On image-heavy sites this can reduce proxy traffic by 50 to 80%. Be careful: some bot-protection scripts check that images and CSS loaded normally, so test that blocking doesn't hurt your success rate.

Avoiding detection: proxies are only half the battle

A clean residential IP won't help if the browser itself announces that it's automated. Anti-bot systems look at:

  • navigator.webdriver and other automation flags
  • Headless-specific quirks in rendering, fonts and plugins
  • Mismatches between IP location and timezone, language and locale
  • TLS and HTTP/2 fingerprints
  • Mouse movement, scrolling and timing

Match the browser to the proxy. If your IP is in Chicago, set the timezone and language to match:

javascript
await page.emulateTimezone('America/Chicago');
await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US,en;q=0.9' });

Many people use puppeteer-extra with the stealth plugin to patch common leaks. It helps, but it isn't a silver bullet against top-tier protection. See Puppeteer stealth and proxies and browser fingerprinting and proxies.

Making the browser match the proxy

A proxy changes where you appear to be. The browser has to agree, or you create the kind of inconsistency that bot protection looks for. These are the settings to align with your exit location.

Timezone

javascript
await page.emulateTimezone('Europe/Berlin');

Check with Intl.DateTimeFormat().resolvedOptions().timeZone inside the page.

Language and locale

Set both the Accept-Language header and Chrome's UI language:

javascript
const browser = await puppeteer.launch({ args: ['--lang=de-DE'] });
// per page:
await page.setExtraHTTPHeaders({ 'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8' });

navigator.language follows Chrome's language setting, which is why the launch flag matters as well as the header.

Geolocation API

If a site asks for the browser's location (some store locators and delivery pages do), grant permission and set coordinates near the exit:

javascript
const ctx = browser.defaultBrowserContext();
await ctx.overridePermissions('https://www.example.com', ['geolocation']);
await page.setGeolocation({ latitude: 52.52, longitude: 13.405 });

Currency and site location settings

Many sites let users pick a country or currency, and remember it in a cookie. Set it explicitly once per session rather than relying on IP detection alone.

Look up the exit dynamically

If you target many locations, look up the exit IP's location at the start of each session (through the proxy) and set timezone and language from it. That keeps everything consistent even when the provider gives you an exit in a neighbouring region.

javascript
async function alignToExit(page) {
  await page.goto('https://ipapi.co/json/', { waitUntil: 'domcontentloaded' });
  const info = JSON.parse(await page.evaluate(() => document.body.innerText));
  if (info.timezone) await page.emulateTimezone(info.timezone);
  return info;
}

Use any IP geolocation service you trust; check its terms and rate limits, and cache results per session.

Cookies, profiles and identities

Proxies give you an IP; cookies and browser profiles give you a history. Managing both together is what makes long-running automation reliable.

One identity, one context

Treat each browser context as one identity: one proxy session, one set of cookies, one timezone and language. Don't share contexts between identities, and don't change a context's proxy mid-life.

Saving and restoring cookies

To keep an identity across runs, save its cookies at the end and restore them next time, along with the same sticky session or static IP.

javascript
import fs from 'node:fs/promises';

// save
const cookies = await page.cookies();
await fs.writeFile('identity-42.json', JSON.stringify(cookies));

// restore (before navigating)
const saved = JSON.parse(await fs.readFile('identity-42.json', 'utf8'));
await page.setCookie(...saved);

Recent Puppeteer versions also offer cookie methods at the browser level; use whichever your version recommends.

Persistent profiles with userDataDir

For identities that need full persistence (local storage, IndexedDB, service workers), give each its own profile directory:

javascript
const browser = await puppeteer.launch({
  userDataDir: './profiles/identity-42',
  args: ['--proxy-server=http://isp-proxy.example.com:8000'],
});

Pair a persistent profile with a static IP (an ISP proxy) or a long sticky session. A profile that reappears from a different country every day looks stranger than a fresh profile.

When to use an antidetect browser instead

If you manage many long-lived identities, especially accounts, an antidetect browser handles profiles, fingerprints and per-profile proxies with less code. Puppeteer can often connect to those browsers' profiles for automation. See antidetect browser proxies and Geonode with antidetect browsers.

A proxy test harness for Puppeteer

Before a big run, or when comparing providers, measure. This harness opens a fresh context per sample, loads a target page, and records whether real content came back.

javascript
import puppeteer from 'puppeteer';

async function testProxy({ server, username, password, url, marker, samples = 50 }) {
  const browser = await puppeteer.launch();
  const results = [];
  for (let i = 0; i < samples; i++) {
    const ctx = await browser.createBrowserContext({ proxyServer: server });
    const page = await ctx.newPage();
    await page.authenticate({ username: username(i), password });
    const t0 = Date.now();
    try {
      const res = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
      const ok = !!res && res.status() < 400 && (await page.content()).includes(marker);
      results.push({ ok, ms: Date.now() - t0 });
    } catch (e) {
      results.push({ ok: false, ms: Date.now() - t0, error: e.message });
    } finally {
      await ctx.close();
    }
  }
  await browser.close();
  const ok = results.filter((r) => r.ok);
  const times = ok.map((r) => r.ms).sort((a, b) => a - b);
  return {
    successRate: ok.length / samples,
    medianMs: times[Math.floor(times.length / 2)],
    p95Ms: times[Math.floor(times.length * 0.95)],
    errors: results.filter((r) => r.error).map((r) => r.error),
  };
}

Pass a username function that returns a new session username per sample, a marker string that only appears on a genuine content page, and run it for each provider and target. Fifty samples gives a rough picture; use several hundred before committing money.

Debugging proxy problems in Puppeteer

When something's wrong and the error message isn't enough, these tools help.

Log every failed request

javascript
page.on('requestfailed', (req) => {
  console.log('FAILED', req.failure()?.errorText, req.url());
});
page.on('response', (res) => {
  if (res.status() >= 400) console.log('HTTP', res.status(), res.url());
});

This shows whether the main document, an API call or a third-party asset failed, which narrows things down quickly.

Run headful

Launch with headless: false and watch. You'll immediately see proxy authentication dialogs, block pages, CAPTCHAs or consent banners that a headless run hides.

Capture a network log

Chrome can write a detailed network log with --log-net-log=/path/netlog.json. Open it in Chrome's NetLog viewer to see proxy resolution, CONNECT tunnels, authentication challenges and connection reuse. It's the definitive tool for "is my traffic actually going through the proxy?" questions.

Compare with cURL

Run the same request through the same proxy with cURL. If cURL works and Puppeteer doesn't, the problem is in the browser setup (authentication, flags, context). If both fail, it's the proxy or the target. See cURL proxy.

Check the exit IP from inside the page

Navigate to an IP echo service in the same context you use for the target. If it shows your server's IP, the proxy isn't applied to that context.

Handling proxy errors in Puppeteer

Common errors and what they mean:

ErrorLikely cause
net::ERR_PROXY_CONNECTION_FAILEDWrong host/port, proxy down, firewall
net::ERR_TUNNEL_CONNECTION_FAILEDProxy refused the CONNECT for HTTPS; often auth or target blocked by provider
407 Proxy Authentication RequiredMissing or wrong credentials, or IP not whitelisted
net::ERR_TIMED_OUTSlow exit node; increase timeout or rotate
net::ERR_EMPTY_RESPONSEProxy dropped the connection; rotate and retry

Wrap navigation in a retry loop that switches session or proxy on failure:

javascript
async function gotoWithRetry(page, url, attempts = 3) {
  for (let i = 1; i <= attempts; i++) {
    try {
      const res = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
      if (res && res.status() < 400) return res;
      throw new Error(`HTTP ${res ? res.status() : 'no response'}`);
    } catch (err) {
      if (i === attempts) throw err;
      await new Promise((r) => setTimeout(r, 1000 * i));
    }
  }
}

More in common proxy errors.

Running Puppeteer with proxies in Docker, CI and the cloud

Most production Puppeteer jobs don't run on a laptop. Each environment has its own proxy quirks.

Docker

Puppeteer publishes an official Docker image, and many teams build their own on top of a Node image with Chrome's system dependencies installed. Proxy-specific points:

  • Pass proxy settings as environment variables at runtime, never bake credentials into the image.
  • Set the proxy explicitly with --proxy-server or a per-context proxyServer. Don't rely on container-level HTTP_PROXY variables to reach Chrome; whether Chrome honours them depends on platform and configuration, so being explicit avoids surprises.
  • Watch shared memory. Chrome uses /dev/shm, which is small by default in Docker. Either increase it (--shm-size) or launch Chrome with --disable-dev-shm-usage. Crashes from low shared memory are often mistaken for proxy failures.
  • Whitelisting: if your provider authenticates by IP, the IP that matters is the container host's outbound IP (or your NAT gateway's), not the container's internal address.

Continuous integration

For end-to-end tests that need proxies (testing geo features, for example):

  • Store credentials as CI secrets and mask them in logs.
  • Keep proxy tests in a separate job, so a provider outage doesn't block every build.
  • Add the smoke test from the "keeping your setup current" section: confirm the exit IP and location before running the real tests.

Serverless functions

Puppeteer can run in serverless functions using puppeteer-core with a Chromium build packaged for the platform. The proxy setup is the same as anywhere else. The constraints are different:

  • Execution time limits cap how long a browser session can last.
  • Cold starts add seconds to the first request, which inflates measured page times.
  • Outbound IPs of serverless platforms are shared and change; IP whitelisting with your proxy provider usually isn't practical, so use username and password authentication.
  • Memory is limited; block heavy resources to keep the browser lean.

Serverless works well for small, bursty jobs such as on-demand screenshots or a handful of pages per request. For sustained crawling, a long-running worker fleet is usually cheaper and more predictable.

Long-running servers

For continuous jobs, a few servers each running a pool of browsers is still the most common setup. Restart browsers periodically (every few hundred pages or every hour) to release memory, and spread contexts across proxy sessions as described in the scaling section.

Puppeteer with Firefox

Puppeteer can drive Firefox as well as Chrome, using the WebDriver BiDi protocol. Proxy configuration in Firefox works differently from Chrome's command-line flag: Firefox reads proxy settings from its preferences.

javascript
const browser = await puppeteer.launch({
  browser: 'firefox',
  extraPrefsFirefox: {
    'network.proxy.type': 1,
    'network.proxy.http': 'gate.example.com',
    'network.proxy.http_port': 7777,
    'network.proxy.ssl': 'gate.example.com',
    'network.proxy.ssl_port': 7777,
  },
});

Some features available with Chrome, including certain per-context proxy options, may behave differently or be unavailable with Firefox, so check the current Puppeteer documentation for the version you use. In practice, most proxy-heavy Puppeteer work runs on Chrome, and teams that need Firefox for fingerprint diversity often use Playwright, which supports Firefox proxies per context directly.

Puppeteer, Playwright or Selenium for proxy work?

All three can use proxies well. The differences are in how much work it takes.

PuppeteerPlaywrightSelenium
Proxy per browser--proxy-server flagproxy launch optionBrowser options
Proxy per contextSupported via createBrowserContextBuilt in, per contextNot built in; usually one proxy per driver
Proxy authenticationpage.authenticate()Credentials in the proxy optionExtensions or a local forwarder, depending on the browser
SOCKS5 with authVia a local forwarderVaries by browserVia a local forwarder
BrowsersChrome, FirefoxChromium, Firefox, WebKitAll major browsers
LanguagesJavaScript and TypeScriptJavaScript, Python, Java, .NETMost languages

If you're starting fresh and proxies matter a lot, Playwright's per-context proxy with credentials is the smoothest experience. If you already use Puppeteer, the per-context proxyServer option plus page.authenticate() covers nearly everything. Selenium is the most flexible on languages and browsers but needs the most setup for authenticated proxies. See Playwright proxies and Selenium proxies.

Planning concurrency

How many browsers, how many contexts, how many proxies? A little arithmetic avoids both overload and waste.

  • Pages per hour ≈ concurrent pages × 3,600 ÷ average seconds per page. Ten concurrent pages at 6 seconds each gives about 6,000 pages an hour.
  • Memory is usually the first limit. A Chrome instance with a few lean contexts uses hundreds of megabytes; heavy pages use more. Measure on your own targets.
  • Per-target politeness is the second limit. Even with thousands of IPs, a site can notice a surge of traffic from your kind of client. Cap concurrency per domain.
  • Proxy plan limits are the third. Some plans cap concurrent connections or threads; check yours.
  • Sessions per IP: on rotating residential, one context per session ID. On static ISP or datacenter lists, keep contexts per IP modest, and spread load across the list.

Start small, measure success rate and memory, then increase concurrency in steps while success stays flat. When success starts falling, you've found the target's tolerance; back off a little and stay there.

Scaling Puppeteer with proxies

Once you're running dozens of browsers, use a pool. puppeteer-cluster manages concurrency, retries and worker browsers; combine it with a proxy per worker or per context. Keep concurrency per IP modest, monitor success rate per proxy, and retire proxies that start failing. See Puppeteer cluster proxies and headless browser proxies.

If you're weighing alternatives, the same concepts apply to Playwright proxies, which has proxy-per-context support built in, and to Selenium proxies.

A production-ready Puppeteer proxy pattern

Here's how the pieces fit together in a small but robust scraper: a rotating residential gateway, one browser context per job, authentication, resource blocking, a location-matched timezone, block detection and retries.

javascript
import puppeteer from 'puppeteer';

const GATE = 'http://gate.example.com:7777';
const PASS = process.env.PROXY_PASS;
const session = () => `customer-USER-country-us-session-${Math.random().toString(36).slice(2, 10)}`;
const BLOCK = new Set(['image', 'media', 'font']);

const browser = await puppeteer.launch({ args: ['--force-webrtc-ip-handling-policy=disable_non_proxied_udp'] });

async function scrape(url, attempts = 3) {
  for (let n = 1; n <= attempts; n++) {
    const ctx = await browser.createBrowserContext({ proxyServer: GATE });
    try {
      const page = await ctx.newPage();
      await page.authenticate({ username: session(), password: PASS });
      await page.emulateTimezone('America/New_York');
      await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US,en;q=0.9' });
      await page.setRequestInterception(true);
      page.on('request', (r) => (BLOCK.has(r.resourceType()) ? r.abort() : r.continue()));

      const res = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
      const html = await page.content();
      const blocked = !res || res.status() >= 400 || /captcha|access denied|verify you are human/i.test(html);
      if (!blocked) return html;
    } catch (err) {
      // network or proxy error: retry with a fresh session
    } finally {
      await ctx.close();
    }
    await new Promise((r) => setTimeout(r, 1000 * n + Math.random() * 1000));
  }
  return null;
}

What each part does:

  • New context per attempt guarantees a fresh connection, so the new session ID really gets a new IP.
  • WebRTC flag prevents your real IP leaking through WebRTC. See WebRTC leaks.
  • Timezone and language match a US exit. Look up the exit's location dynamically if you target many regions.
  • Resource blocking cuts residential bandwidth costs.
  • Block detection treats CAPTCHA pages as failures even when the status code is 200.
  • Backoff with jitter avoids hammering the target.

Proxy tip: Want to skip proxy management on some targets? Geonode offers both raw proxies and a Scraper API that returns HTML or Markdown with optional JavaScript rendering. We compare the two approaches in using Geonode with Puppeteer.

Managing a pool of proxies in Puppeteer

With a rotating residential gateway, the provider manages the pool and you just change session IDs. With a list of static proxies (ISP or datacenter), you manage the pool yourself. A small pool manager makes that much easier.

What a pool manager does

  • Hands out proxies to new contexts according to a strategy.
  • Tracks health: recent successes, failures and response times per proxy.
  • Benches failing proxies temporarily, and retires ones that keep failing.
  • Limits concurrency per proxy, so no single IP gets overloaded.
  • Remembers assignments for identities that must keep the same proxy.

A minimal implementation

javascript
class ProxyPool {
  constructor(proxies, { maxPerProxy = 2, benchMs = 10 * 60 * 1000 } = {}) {
    this.items = proxies.map((p) => ({ ...p, inUse: 0, ok: 0, fail: 0, benchedUntil: 0 }));
    this.maxPerProxy = maxPerProxy;
    this.benchMs = benchMs;
  }
  acquire() {
    const now = Date.now();
    const free = this.items
      .filter((p) => p.benchedUntil < now && p.inUse < this.maxPerProxy)
      .sort((a, b) => a.inUse - b.inUse || b.ok - b.fail - (a.ok - a.fail));
    if (!free.length) return null;
    free[0].inUse++;
    return free[0];
  }
  release(p, success) {
    p.inUse--;
    if (success) p.ok++;
    else if (++p.fail % 3 === 0) p.benchedUntil = Date.now() + this.benchMs;
  }
}

Each proxy object holds server, username and password. Acquire one before creating a context, pass server as the context's proxyServer, authenticate with its credentials, and release it with the outcome when the context closes.

Selection strategies

  • Least-busy first (as above) spreads load evenly.
  • Round-robin is simplest but ignores health.
  • Weighted by success prefers proxies that have been working, at the risk of overusing a few.
  • Sticky per identity always gives the same proxy to the same account or profile. Essential for logged-in work.
  • Sticky per domain keeps one domain on a subset of proxies, useful when a site tolerates a few consistent IPs better than many changing ones.

Mix strategies: sticky per identity for accounts, least-busy for stateless scraping.

Health checks before use

For static lists, a quick health check at startup saves failures later. Request a lightweight page (an IP echo endpoint) through each proxy with a short timeout, record the exit IP and time, and bench anything that fails. Don't health-check against your real target repeatedly; that creates unnecessary traffic and can flag IPs.

Retiring proxies

Track failures per proxy per target. A proxy that fails on one site may be fine on others; retire it only for that site. A proxy that fails everywhere is dead, so remove it and ask your provider for a replacement.

Puppeteer, bot protection and the limits of proxies

It's worth repeating, with more detail, because it's the most common source of frustration: proxies solve the IP part of detection, and only that part.

What modern protection checks in a Puppeteer session

  • Automation markers such as navigator.webdriver, which is true in automated Chrome unless changed.
  • Headless differences in rendering, fonts, media capabilities and APIs. Chrome's newer headless mode is much closer to normal Chrome than the old one, but differences remain.
  • Fingerprint consistency: user agent, platform, screen, GPU, fonts, timezone and language should form a believable combination.
  • TLS and HTTP/2 fingerprints: Puppeteer uses real Chrome, so these match Chrome. That's an advantage over plain HTTP clients.
  • Behaviour: instant navigation, no mouse movement, perfectly regular timing.
  • IP reputation and type: where proxies come in.

A sensible order of fixes

  1. Use a recent Chrome in the newer headless mode, or headful under a virtual display for the hardest sites.
  2. Align locale, timezone and language with the exit IP, as shown earlier.
  3. Pace actions with realistic delays and some variation.
  4. Choose the right proxy type for the target, starting with the cheapest that works.
  5. Consider stealth patches carefully: they can help, but some popular plugins are unmaintained and may introduce their own inconsistencies.
  6. For the hardest targets, compare the cost of continued engineering with an unblocker or scraping API.

See Puppeteer stealth and proxies, browser fingerprinting and proxies and the Geonode Scraper API.

Staying within the rules

Bypassing bot protection on a site that has clearly said no can have legal and contractual consequences, and some uses (logging into others' accounts, credential testing, scraping personal data at scale) are off-limits whatever the technique. Use automation for legitimate purposes, respect rate limits and robots.txt, and read ethical web scraping.

Choosing between Puppeteer proxy approaches

Your situationRecommended approach
One proxy, fixed server IPIP whitelisting + --proxy-server
Rotating residential, many pagesPer-context proxy + new session ID per context
Static ISP or datacenter listPer-context proxy, cycle through the list
Authenticated SOCKS5proxy-chain local forwarder
Many accounts with distinct identitiesSeparate browser profiles or an antidetect browser
Thousands of pages, high concurrencypuppeteer-cluster with per-worker sessions. See Puppeteer cluster proxies

Measuring whether your Puppeteer proxies are working

Track four numbers per target:

  1. Success rate: pages that returned real content.
  2. Median and p95 page time.
  3. Bytes per successful page (for per-GB plans).
  4. Retries per success.

If success is low but pages are fast, you're probably being blocked: look at fingerprints and IP type. If success is high but pages are slow, look at exit locations and resource blocking. If bytes per page are high, block more resources or switch to ISP proxies with unlimited bandwidth. For benchmarking methods, see proxy speed test and how to test proxies before buying.

Choosing a proxy provider for Puppeteer

Most proxy providers work with Puppeteer, but a few features make some providers much easier to use with headless browsers than others.

  • Session control in the username or port. You want to choose a new sticky session per browser context with a simple change to the username or port. Gateways that only rotate on a timer you can't control are harder to work with.
  • Reliable sticky sessions at the length your flows need. Browsers hold sessions for minutes; test that sessions hold.
  • Generous concurrency. Browser automation opens many connections in parallel. Check plan limits on concurrent connections.
  • HTTP proxy support with username and password. Chrome handles authenticated HTTP proxies well through page.authenticate(). SOCKS5 with authentication needs a local forwarder, so HTTP is simpler.
  • IP whitelisting for fixed-IP servers, which removes the need for authentication entirely.
  • Clear usage statistics, because browser traffic is heavier than you expect and you'll want to see it.
  • Several proxy types, so you can move logged-in flows to static IPs and easy targets to datacenter IPs without a second account.
  • A managed fallback such as a scraping API with JavaScript rendering, for targets where your own browser isn't worth the effort.

Geonode ticks most of these boxes: per-context sessions through the username, sticky sessions of up to 24 hours, HTTP, HTTPS and SOCKS5 support, IP whitelisting, residential, ISP and datacenter products, and a Scraper API. As always, test it alongside alternatives on your own targets before committing. See best proxies for web scraping and headless browser proxies.

Anti-patterns in Puppeteer proxy code

These patterns appear in a lot of tutorials and Stack Overflow answers. They work in demos and cause trouble in production.

Launching a new browser for every page

It works, but it's slow and memory-hungry, and it hides the real problem (usually needing a fresh IP). Use browser contexts with their own proxy and session instead.

Putting credentials in --proxy-server

Chrome ignores them, and the resulting 407 errors confuse people into thinking the proxy is broken. Use page.authenticate(), whitelisting or a forwarder.

Rotating IPs inside a logged-in session

Changing session IDs between page loads of the same account makes the site see one user teleporting between cities. Keep one IP per identity for its whole session.

Treating HTTP 200 as success

Block pages, CAPTCHAs and soft errors often return 200. Check for real content.

Infinite retries

A loop that retries until success can burn gigabytes on a blocked target. Cap retries and back off.

Using a node-side proxy library to fetch page requests

Re-issuing browser requests from Node through a proxy library changes the TLS fingerprint and headers, making traffic look less like Chrome. Let Chrome's own network stack use the proxy.

Ignoring time zones and languages

A US residential IP with a browser reporting a European timezone is an easy inconsistency to spot. Align them.

Leaving WebRTC and QUIC unrestricted

Your real IP can leak, or traffic may not take the path you expect. Restrict both when every request must go through the proxy.

No per-domain limits

High concurrency against one site gets IPs flagged and pools degraded. Cap concurrency per domain.

Logging full proxy URLs

They contain passwords, and logs travel further than you think. Mask them.

Moving from older patterns to per-context proxies

If your codebase predates per-context proxy support, you may be launching one browser per proxy or using a request-level proxy library. Migrating is straightforward:

  1. Wrap context creation in a function that takes proxy details and returns a ready, authenticated page.
  2. Replace "new browser per proxy" with "new context per proxy" inside a small number of long-lived browsers.
  3. Remove request-level proxy libraries and let Chrome handle the proxy.
  4. Move launch flags that varied per identity (such as language) into per-page settings where possible, or keep a separate browser for each language you need.
  5. Re-run your test harness to confirm success rates and exit IPs are unchanged.

Most teams see lower memory use, faster startup and fewer odd fingerprint issues after the switch.

Puppeteer proxy costs at a glance

WorkloadProxy choiceCost driver
10,000 product pages/month, protected retailerRotating residential, assets blockedGB per page (often 0.3 to 1 MB)
50 logged-in sessions running all dayISP proxies, one per sessionPer IP per month
100,000 pages/month, lenient siteDatacenter listPer IP; bandwidth usually free
Hard target with frequent CAPTCHAsUnblocker or mobile proxiesPer success or per GB

For more budget techniques, read cheap proxies, and for proxy type selection, the best proxies pillar guide.

Keeping your Puppeteer proxy setup current

Puppeteer moves quickly; as of October 2026 it's on its 25.x releases, and each major version tracks a newer Chrome. Proxy code that worked a year ago usually still works, but a few habits stop upgrades from surprising you:

  • Pin versions in package.json and upgrade deliberately, not as a side effect of a fresh install.
  • Re-run a proxy smoke test after every upgrade: launch with your proxy, authenticate, load an IP echo page in a normal page and in a new browser context, and confirm the exit IP and location.
  • Check the official API pages for the features you rely on. Page.authenticate and the proxyServer and proxyBypassList options in BrowserContextOptions are the two that matter most for proxies.
  • Review third-party plugins. Some popular add-ons are no longer maintained; the stealth plugin's last release was in 2023. See Puppeteer stealth and proxies.
  • Watch Chrome's own changes. Headless mode, extension support and network features evolve with each Chrome release. See headless browser proxies.

Keep the smoke test in your repository and run it in CI. Five minutes of automated checking beats discovering on a Monday morning that every request has been leaving from your server's own IP since Friday's deploy.

For the auth details, see Puppeteer proxy authentication; for scaling, puppeteer-cluster proxies.

Worked example: a JavaScript-heavy store, end to end

Here's how a real project comes together, from first test to steady production. The target is a mid-sized online store whose product listings load through JavaScript and which uses commercial bot protection.

1. Try without a browser

Before reaching for Puppeteer, open the store in a normal browser with developer tools and look at the network requests. Often, listings come from a JSON endpoint you can call directly with an HTTP client, which is far cheaper. In this example, the listing API requires a token generated by a script on the page, so a browser is genuinely needed.

2. Test proxy types

Run the test harness from earlier with 50 samples each on datacenter, rotating residential and ISP proxies. Datacenter IPs get challenged on almost every request. Residential and ISP both succeed above 90%. Since the job is stateless (independent category pages), rotating residential is chosen for diversity.

3. Design sessions

Each browser context gets a new residential session and handles five category pages before closing. That reuses the token and cookies for a few pages, which looks more natural than a new identity per page, while keeping each identity short-lived.

4. Trim bandwidth

Blocking images, media and fonts cuts page weight from about 3 MB to about 600 KB. Blocking third-party analytics and chat widgets saves another 150 KB. One bot-protection script must load, so it's allowed. Success rate is unchanged.

5. Align the browser

The exit country is fixed (the store's home market), so timezone and Accept-Language are set to match. WebRTC is restricted and QUIC is disabled so all traffic uses the proxy.

6. Detect blocks

A page counts as successful only if the product grid element exists and contains items. Challenge pages, empty grids and error pages trigger a retry with a new context and session, up to three times.

7. Set concurrency

Starting at five concurrent contexts and increasing in steps, success stays flat up to fifteen and begins to drop at twenty. Production runs at twelve.

8. Monitor

Each run logs success rate, median and p95 page time, bytes per successful page and retries per success. An alert fires if success falls below 85% or bytes per page rise by more than 50%.

The result

Around 20,000 category pages a day at roughly 650 KB per page is about 13 GB of residential traffic a day before overhead. That's the number to budget for, and the one to attack next: the team later finds that two of the five pages per context can be replaced by a JSON call using the token the browser already obtained, cutting traffic by a third.

The pattern generalises: test whether you need a browser at all, pick the proxy type by testing, design sessions around the site's state, trim bandwidth, align the browser, detect blocks properly, find the concurrency limit, and monitor.

Common Puppeteer proxy recipes

Screenshots from different countries

To capture how a page looks in several markets, loop over countries, create a context per country with matching timezone and language, and save a screenshot. Leave images enabled, since the screenshot needs them. City-targeted residential proxies help for local content. This is a common task in ad verification and localization testing.

PDFs of geo-specific pages

The same approach works for page.pdf(), for example to archive local terms, prices or disclosures as evidence. Record the exit IP and its location alongside each PDF.

Monitoring a page for changes

Load the page through a sticky session on a schedule, extract the part you care about, hash it, and alert when the hash changes. Use the lightest possible fetch: if the content is in the HTML, use an HTTP client rather than Puppeteer.

Checking redirects and landing pages

Affiliate links, ad click-throughs and short links can redirect differently by location and device. Log page.on('response') events to record the redirect chain from each location. See Geonode for affiliate link testing.

Logged-in workflows

Use a static ISP proxy or a long sticky session, a persistent profile directory, and slow, human-paced actions. Save cookies after each run. Never rotate IPs inside a logged-in session.

Collecting search results

Search engines are strict with automated browsers. Use rotating residential with city targeting, low rates, and the search engine's own location parameters, or consider a SERP API. See Google SERP scraping proxies.

Keeping proxy credentials safe in Puppeteer projects

  • Environment variables or a secrets manager, never hard-coded strings in source files.
  • Don't log full proxy URLs. They contain passwords. Log the host and a masked username instead.
  • Watch error messages. Some libraries include the proxy URL in exceptions; sanitise before sending errors to tracking services.
  • Separate credentials per environment (development, staging, production) with their own traffic limits.
  • Rotate passwords when people leave the team or a laptop is lost.
  • Prefer IP whitelisting for fixed-IP servers, and remove old IPs when servers are retired.

More Puppeteer proxy questions, answered

Can I change the proxy of an existing page?

Not directly. A page's proxy is fixed by its browser context (or the browser's launch flags). To switch, close the context and open a new one with the new proxy. That's also the cleanest way to get a fresh IP from a rotating gateway.

Do I need to call page.authenticate() on every page?

Yes, for each new page that will make requests through an authenticated proxy. Call it before the first navigation. Pages opened by the site itself (pop-ups, new tabs from links) are new pages too; listen for the targetcreated event or the context's page event and authenticate them as well if you need them.

Why do I get a 407 even though my credentials are right?

Common causes: authenticating after navigation has started, special characters in the password that need different handling in your provider's format, a pop-up page that wasn't authenticated, or IP whitelisting enabled on the provider side with a different server IP. Test the same credentials with cURL to rule out the account itself.

Can I use different proxies for the main page and its iframes?

No. All requests from a context use that context's proxy, including iframes and workers. If you need different exits for different parts of a page, you need separate contexts loading those parts separately.

Does Puppeteer's request interception affect the proxy?

Aborting requests stops them before they reach the proxy, which saves bandwidth. Continuing them sends them through the proxy as normal. Responding to requests from your own code (mocking) means they never reach the proxy at all. Interception doesn't change which proxy is used.

Does blocking images make me easier to detect?

Sometimes. Some bot protection scripts check that images and stylesheets loaded, or measure layout. If blocking hurts your success rate on a target, allow the specific resources the protection needs and keep blocking the rest.

How do I use an ISP proxy list with Puppeteer?

Load the list into a pool manager like the one above, give each context its own proxy, and authenticate with the list's credentials. For accounts, assign each account a fixed proxy from the list.

Is it better to use one browser with many contexts or many browsers?

Many contexts in one browser is more memory-efficient and starts faster. Separate browsers isolate crashes and allow different launch flags per identity (such as different --lang values). A common compromise is a few browsers, each with several contexts.

How do I rotate IPs without closing the page?

With a rotating gateway that rotates per request, some requests in the same page will use different IPs, but connection reuse makes this unpredictable. For a controlled switch, close the context and open a new one. Changing IPs in the middle of a page's life is usually a bad idea anyway, since the site sees one session jump between addresses.

Why are pages slower through the proxy than direct?

Every request takes extra hops: to the gateway, to the exit, to the site. Residential exits add latency. Block unnecessary resources, use exits near the target, run your workers near the gateway region, and wait for domcontentloaded rather than full network idle when you can.

Can Puppeteer use a proxy that requires a client certificate?

Not through the standard proxy settings. A local forwarding proxy that handles the certificate, with Puppeteer pointed at the forwarder, is the usual workaround.

Does Puppeteer support IPv6 proxies?

Chrome can connect to proxies over IPv6 and through proxies to IPv6 sites. Whether you get an IPv6 exit depends on the provider. Remember that many sites are IPv4-only. See IPv6 proxies.

How do I make Puppeteer use the system proxy?

Chrome on desktop platforms uses system settings when no proxy flag is given, but in servers and containers there usually are no system settings. In automation, always set the proxy explicitly so the behaviour is predictable.

Should I use a scraping API instead of Puppeteer plus proxies?

If you only need a few thousand pages from hard targets, an API that renders JavaScript and returns HTML or Markdown can be cheaper once you count engineering time. For large volumes or complex interactions, your own Puppeteer setup is usually cheaper per page and more flexible. See scraping API vs proxies.

Puppeteer proxy glossary

TermMeaning
--proxy-serverChrome launch flag setting a proxy for the whole browser.
proxyServerBrowser context option setting a proxy for one context.
proxyBypassListHosts that skip the proxy.
page.authenticate()Supplies credentials when the proxy asks for them.
CONNECT tunnelHow HTTPS traffic passes through an HTTP proxy.
Browser contextAn isolated, incognito-like session inside one browser.
Sticky sessionA proxy session that keeps the same exit IP for a period.
Local forwarderA proxy on your machine that adds credentials and forwards upstream, such as proxy-chain.
Request interceptionPuppeteer's ability to abort, modify or mock requests.
WebRTC leakExposure of your real IP through WebRTC.
QUIC / HTTP/3UDP-based transport that HTTP proxies usually don't carry.
NetLogChrome's detailed network log, useful for debugging proxies.

Your Puppeteer proxy checklist

Setup

  • Proxy set explicitly per context, not left to system defaults
  • Authentication on every page, including pop-ups you use
  • Credentials in environment variables, masked in logs
  • WebRTC restricted and QUIC disabled where all traffic must use the proxy

Identity

  • One context per identity and proxy session
  • Timezone, language and locale matched to the exit
  • Cookies or profiles persisted only together with a consistent IP

Efficiency

  • HTTP client used instead of a browser wherever possible
  • Images, media, fonts and third parties blocked where safe
  • domcontentloaded used where full load isn't needed

Reliability

  • Content-based block detection
  • Retries with a fresh context and session, capped
  • Pool manager with health tracking for static lists
  • Per-domain concurrency limits

Operations

  • Success, latency, bytes per success and retries monitored per target
  • Smoke test after every Puppeteer or Chrome upgrade
  • A backup provider configured and tested monthly

If you need a proxy network to run this checklist against, Geonode supports both rotating and sticky residential sessions, static ISP proxies and rotating datacenter traffic, which covers every Puppeteer pattern in this guide.

Controlling Puppeteer proxy costs at scale

Browser automation is the most bandwidth-hungry way to use proxies, so cost control deserves its own plan.

  • Measure bytes per successful page per target. It's the single most useful cost metric. Your provider's usage statistics, divided by your successful page count, give you the real number including overhead.
  • Budget per target. Give each domain a daily traffic budget and stop or slow the job when it's reached.
  • Prefer HTTP clients after the browser has done its job. If the browser is only needed to obtain a token or cookie, use it for that and then fetch data endpoints with a lightweight client through the same proxy session.
  • Reuse contexts for a few pages. Loading a site's scripts and styles once per context instead of once per page saves a lot, as long as a handful of pages per identity suits the site.
  • Cache static assets yourself where appropriate, or allow the browser's cache within a context.
  • Route by target. Lenient sites go through datacenter proxies; only protected sites use residential.
  • Watch retries. Retries per success above about 0.3 usually mean something's wrong: blocks, timeouts or a parser treating good pages as failures.
  • Consider flat-rate plans only after you've trimmed bandwidth. A flat plan makes waste invisible, not cheaper.

Teams that apply these consistently often cut residential bandwidth for browser jobs by more than half without losing data. Our cheap proxies guide has more techniques and a cost calculator.

A geo QA workflow for your own product

One of the most valuable and least risky uses of Puppeteer with proxies is checking your own website or app in each market you serve.

What to check

  • Language and translations on key pages
  • Currency, prices, taxes and shipping options
  • Legal notices, cookie banners and consent flows required in each region
  • Geo-redirects and country selectors
  • Payment methods offered at checkout
  • Content restrictions and region-specific features
  • Page speed from each region

How to run it

  1. List markets and pages. Start with the home page, a category page, a product page, the cart and the checkout's first step.
  2. Create a context per market, with an exit in that country and timezone and language to match.
  3. Load each page, take a screenshot, and extract the values you care about (currency symbol, price, banner text).
  4. Compare against expectations stored in a simple file per market.
  5. Report differences with screenshots attached.

Which proxies

For your own site, datacenter proxies in each country are often enough, because you won't block yourself. Use residential exits when third-party services in the flow (payment providers, maps, ads, fraud checks) treat datacenter traffic differently from real users. See Geonode for localization testing and Geonode for website monitoring.

Making it routine

Run the workflow before each release and on a weekly schedule. Most regional bugs are found by customers; a scheduled check finds them first.

SEO audits with Puppeteer and proxies

SEO teams use Puppeteer to see pages the way search engines and users in different places see them.

  • Rendering checks. Load JavaScript-heavy pages and confirm that key content and links appear in the rendered DOM.
  • Geo-redirect checks. Visit from different countries and confirm that redirects, hreflang tags and country selectors behave as intended, and that search engine crawlers aren't being redirected away from content.
  • Local results. Check how search results and map listings look in specific cities, using city-targeted residential exits and the search engine's location parameters. Keep rates very low.
  • Competitor monitoring. Track changes to competitors' titles, headings and structured data over time.
  • Performance from the field. Measure load times and Core Web Vitals-style metrics from different regions to spot slow markets.

For rank tracking at volume, a SERP API is usually more reliable than browsers. For audits and spot checks, Puppeteer with residential proxies gives you the real rendered view. See best proxies for SEO rank tracking, SEO proxies and Geonode for SEO rank tracking.

When Puppeteer isn't the right tool

Puppeteer is powerful, but it's often more than a job needs.

  • The data is in the HTML. Use an HTTP client; it's faster and far cheaper through proxies.
  • There's a JSON endpoint. Call it directly.
  • You need Firefox or WebKit diversity, or Python. Playwright supports more browsers and languages.
  • You need hundreds of thousands of pages a day from easy targets. Browsers are expensive at that scale; a crawler with an HTTP client wins.
  • The target is extremely hard and volume is low. A scraping API with JavaScript rendering may be cheaper than maintaining your own stealth setup.

Use Puppeteer where a real browser is genuinely needed, and pair it with the cheapest proxy that works for each target.

Running Puppeteer proxies as shared team infrastructure

When several people or services in an organisation use Puppeteer with proxies, a little shared infrastructure saves a lot of duplicated effort and surprise bills.

  • A shared browser-and-proxy library. One internal package that creates authenticated contexts, aligns timezone and language, blocks resources, detects blocks and handles retries. Every project uses it, so fixes and improvements reach everyone.
  • Central proxy configuration. Providers, gateways, targeting syntax and credentials live in one configuration service or secrets store. Projects ask for "residential, US, sticky 10 minutes" rather than hard-coding a provider's username format.
  • Sub-users per project with traffic limits, so usage can be attributed and capped.
  • A shared dashboard of success rate, bytes per success and spend per project and target.
  • A browser service. Some teams run a pool of browsers as a service that other code connects to over the DevTools protocol, with proxies configured per session. That concentrates the tricky parts (memory, crashes, upgrades) in one place.
  • Upgrade discipline. One owner tests new Puppeteer and Chrome versions against the shared smoke test before projects upgrade.

This doesn't need to be elaborate. Even a shared helper module and a single usage dashboard make a big difference once three or four projects depend on browser automation.

Logging that makes proxy problems obvious

Good logs turn "the scraper is broken" into "target X started returning challenge pages from residential exits in Germany at 14:00". For each page attempt, log:

  • Timestamp, project and target domain
  • Proxy provider, product and a masked session identifier
  • Requested location and, for a sample of requests, the actual exit IP and its location
  • Outcome: success, block, timeout, proxy error or parse failure
  • HTTP status of the main document
  • Page time and bytes transferred
  • Retry number

With those fields, you can group failures by provider, location, target and time, which is almost always enough to find the cause. Keep raw logs for a short period and aggregated metrics for longer.

Browser automation and the rules

Because Puppeteer drives a real browser, it can do anything a person can do on a website, at machine speed. That makes it worth being deliberate about the rules.

  • Public pages versus logged-in areas. Automating access to public pages carries less risk than automating logged-in areas, especially other people's accounts. Only automate accounts you own or are authorised to use.
  • Terms of service. Many sites restrict automated access in their terms. Know what you've agreed to, particularly if you created an account.
  • Personal data. Rendering pages makes it easy to collect personal data you didn't intend to. Filter it out early, and follow data protection rules for anything you keep.
  • Load. A browser loads many resources per page. Polite rates matter even more than with lightweight clients.
  • Purpose. Testing, monitoring, research and legitimate data collection are fine. Fraud, credential stuffing, fake engagement and evading bans for abusive behaviour are not, and reputable proxy providers will close accounts used for them.

See are proxies legal? and ethical web scraping for more.

Puppeteer proxy setups by experience level

If you're just starting

Use one authenticated proxy with --proxy-server and page.authenticate(), confirm the exit IP on an IP echo page, and then load your target. Add resource blocking once it works. Don't worry about pools or contexts yet. A small pay-as-you-go residential balance is enough to learn on, and it's worth trying a datacenter proxy on the same target to see the difference for yourself.

If you're running real jobs

Move to one browser with a context per job, a new sticky session per context, timezone and language aligned to the exit, content-based block detection and capped retries. Put proxy settings in configuration and log outcomes per page. This is the level the production pattern earlier in the guide is written for, and it covers most teams' needs for a long time.

If you're operating at scale

Add a queue, a worker fleet, per-domain concurrency and traffic budgets, a pool manager for static lists, monitoring with alerts, a backup provider and an upgrade smoke test in CI. At this level, the biggest gains usually come from replacing browser fetches with lighter requests wherever the browser isn't strictly needed, and from routing each domain to the cheapest proxy type that works.

Whatever your level, the next step up is usually small: one more habit, one more metric, one more safeguard. Add them one at a time and measure the effect of each, and your Puppeteer proxy setup will keep getting cheaper and more reliable as it grows.

Five-minute Puppeteer proxy health check

When a job starts misbehaving, run through these five checks before changing anything else. Together they take about five minutes and, in our experience, find the cause of most proxy-related failures without any guesswork.

  1. Is the proxy applied? Load an IP echo page in the same context as the job. If it shows your server's own IP, the context has no proxy, or the page wasn't authenticated before navigating.
  2. Is the account healthy? Check the provider dashboard for remaining balance, traffic limits on the sub-user, and any service notices.
  3. Is it the target or the proxy? Load a neutral page, such as a well-known site's home page, through the same context. If that works and the target doesn't, the target is blocking or has changed.
  4. What does the failure look like? Run once with headless: false or take a screenshot on failure. A CAPTCHA, a consent banner, a login wall or an empty page each points to a different fix.
  5. Did anything change on your side? A Puppeteer or Chrome upgrade, a new launch flag, a different server region or a change to resource blocking is often the real culprit.

Write down what you found, what you changed and whether it fixed the problem. Over time, these notes turn into a runbook that lets anyone on the team, including people who didn't write the original code, fix the most common proxy problems quickly and confidently.

If none of the five checks explains the problem, capture a network log with --log-net-log and compare a failing run with a working one from before the problem started. The difference is almost always visible there: a missing authentication step, a request going direct instead of through the proxy, or a tunnel the proxy refused.

Final thoughts

Proxies and Puppeteer fit together well once you understand three things: Chrome applies proxies per browser or per context, not per request; identities are made of an IP, a browser profile and consistent settings together; and most cost comes from what the browser downloads. Get those right, add proper block detection and retries, and measure success per target, and the choice of provider becomes a matter of testing rather than guesswork.

For a provider to test with, Geonode supports per-context sticky sessions, residential, ISP and datacenter proxies, and a Scraper API for the targets you'd rather not run a browser against. For related tools, see Playwright proxies, Selenium proxies and headless browser proxies, and for the bigger buying picture, the best proxies guide.

FAQ

Can I put the username and password in --proxy-server?

No. Chrome ignores credentials in the flag. Use page.authenticate(), IP whitelisting, or a local forwarder such as proxy-chain.

What are the best proxies for Puppeteer?

Rotating residential proxies with sticky sessions are the best general choice; ISP proxies are best for long, logged-in sessions. Datacenter proxies are fine for lenient targets. See best proxies for web scraping.

Why does Puppeteer still get blocked with residential proxies?

Usually because of browser fingerprinting, behaviour patterns or rate, not the IP. See Puppeteer stealth and proxies.

How do I check which IP Puppeteer is using?

Navigate to an IP echo service such as https://httpbin.org/ip or https://api.ipify.org and read the page text. See how to check if a proxy works.

Does Puppeteer support SOCKS5 with authentication?

Not directly. Chrome doesn't support SOCKS5 auth. Use IP whitelisting or a local forwarder. See Puppeteer SOCKS5 proxy.

Affiliate disclosure: Team of Monkeys is a Geonode affiliate. Links and banners marked as sponsored, and links to Geonode in the text, use our referral link, and we may earn a commission if you buy through them, at no extra cost to you. Our explanations and comparisons are written to be useful whichever provider you choose.

Editorial note: Proxy providers change their pools, features and prices often, so treat any figures here as typical ranges at the time of writing and confirm current terms with the provider before you buy.
Shopping for proxies?

Start with our most-read buying guides