Web Scraping & Automation

Rotating Proxies in Puppeteer: Patterns That Work

How to rotate proxies in Puppeteer: provider-side session rotation, rotating static proxy lists per browser or context, ban detection, retries and choosing rotation frequency.

By Team of Monkeys Editorial Updated 4 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.

Rotation in a browser is different

Rotating proxies in a simple HTTP scraper is easy: pick a new proxy for each request. In Puppeteer, it's trickier. A browser loads dozens of resources per page over persistent connections, holds cookies and challenge tokens, and applies its proxy setting browser-wide by default. Rotating mid-page would break things; rotating too rarely would hit rate limits.

The right unit of rotation in Puppeteer is usually the browser context (or the browser), not the request. This guide shows the patterns. For theory, see proxy rotation strategies.

Pattern 1: Provider-side rotation with session IDs

With a rotating residential gateway, you keep one proxy host and change the session ID in the username to get a new IP. Combine with a new browser context per session so Chrome doesn't reuse old connections:

javascript
import puppeteer from 'puppeteer';

const GATE = 'http://gate.example.com:7777';
const newSession = () => `customer-USER-country-us-session-${Math.random().toString(36).slice(2, 10)}`;

const browser = await puppeteer.launch();

async function scrapeWithFreshIp(url) {
  const ctx = await browser.createBrowserContext({ proxyServer: GATE });
  try {
    const page = await ctx.newPage();
    await page.authenticate({ username: newSession(), password: 'PASS' });
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
    return await page.content();
  } finally {
    await ctx.close();
  }
}

Each call gets a new context, a new session ID and therefore a new residential IP. See rotating residential proxies.

Pattern 2: Rotating through a static proxy list

With ISP or datacenter proxies, cycle through a list. Use one context per proxy:

javascript
const proxies = [
  { server: 'http://203.0.113.10:8080', username: 'u', password: 'p' },
  { server: 'http://203.0.113.11:8080', username: 'u', password: 'p' },
  { server: 'http://203.0.113.12:8080', username: 'u', password: 'p' },
];
let i = 0;
const nextProxy = () => proxies[i++ % proxies.length];

async function scrape(url) {
  const proxy = nextProxy();
  const ctx = await browser.createBrowserContext({ proxyServer: proxy.server });
  try {
    const page = await ctx.newPage();
    await page.authenticate({ username: proxy.username, password: proxy.password });
    await page.goto(url, { waitUntil: 'domcontentloaded' });
    return await page.content();
  } finally {
    await ctx.close();
  }
}

On older Puppeteer versions without per-context proxies, launch one browser per proxy instead.

Pattern 3: Rotate on failure

Rotation is most valuable when an IP gets blocked. Detect blocks and retry with a fresh IP:

javascript
function looksBlocked(status, html) {
  return status === 403 || status === 429 ||
    /captcha|access denied|unusual traffic|verify you are human/i.test(html);
}

async function scrapeWithRetry(url, attempts = 4) {
  for (let n = 1; n <= attempts; n++) {
    const ctx = await browser.createBrowserContext({ proxyServer: GATE });
    try {
      const page = await ctx.newPage();
      await page.authenticate({ username: newSession(), password: 'PASS' });
      const res = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
      const html = await page.content();
      if (res && !looksBlocked(res.status(), html)) return html;
    } catch (e) {
      // network error: fall through to retry
    } finally {
      await ctx.close();
    }
    await new Promise((r) => setTimeout(r, 1000 * n));
  }
  throw new Error(`blocked after ${attempts} attempts: ${url}`);
}

How often should you rotate?

WorkloadRotate
Independent product or listing pagesNew context per page (or per few pages)
Paginated searchOne context for the pagination run
Logged-in sessionDon't rotate; use a static ISP IP
Challenge-protected sitesKeep the IP that passed the challenge for as long as it works

See sticky vs rotating sessions.

Keep fingerprints consistent per IP

When you rotate IPs, rotate the matching identity too: user agent, viewport, timezone and language should be plausible for the IP's location and stay consistent within a session. See Puppeteer stealth and proxies.

Scaling up

For dozens of concurrent browsers, use puppeteer-cluster or your own pool, with proxy selection per worker or per task. See Puppeteer cluster proxies.

Proxy tip: If you need a pool to run this against, Geonode works with every tool in this guide: rotate per request for stateless pages, or hold a sticky session for logins and pagination. See using Geonode with Puppeteer for working code.

Rotating ISP proxies for logged-in workflows

Rotation isn't always about scraping. If you manage several logged-in accounts with Puppeteer, "rotation" means assigning each account its own static ISP proxy and context, and never mixing them:

javascript
const accounts = [
  { name: 'acct-01', proxy: { server: 'http://203.0.113.21:8000', username: 'u', password: 'p' } },
  { name: 'acct-02', proxy: { server: 'http://203.0.113.22:8000', username: 'u', password: 'p' } },
];

for (const acct of accounts) {
  const ctx = await browser.createBrowserContext({ proxyServer: acct.proxy.server });
  const page = await ctx.newPage();
  await page.authenticate({ username: acct.proxy.username, password: acct.proxy.password });
  // load saved cookies for acct.name, do work, save cookies
  await ctx.close();
}

For long-lived identities with distinct fingerprints, consider persistent profiles or an antidetect browser.

Monitoring rotation in production

Log for each context: proxy or session ID, exit IP (check once at context start in development), target, status, block detection and duration. Watch for:

  • Repeated exit IPs with a rotating gateway (connection reuse or thin pools).
  • Rising block rates on one proxy or subnet (time to retire it).
  • Slow contexts (exit quality or location).

Bandwidth and rotation

Each new context starts with a cold cache, so the first page load downloads everything. Rotating too often increases bandwidth on per-GB plans. If a target tolerates it, reuse a context for several pages before rotating, and block heavy resources. See cheap proxies.

Choosing context lifetimes

Target behaviourContext lifetime
Tolerant, no challengesReuse a context for many pages, rotate periodically
Challenge on first visit, cookie-based clearanceKeep context while clearance lasts
Strict per-IP limitsRotate every few pages
Logged-in flowsOne context per account, no rotation

Measure success over the first few hundred pages with different lifetimes and pick the longest one that keeps success high; longer lifetimes save bandwidth and setup time.

Rotation with geography

If you rotate across countries, rotate the matching browser settings too:

javascript
const MARKETS = [
  { cc: 'us', tz: 'America/New_York', lang: 'en-US' },
  { cc: 'gb', tz: 'Europe/London', lang: 'en-GB' },
  { cc: 'de', tz: 'Europe/Berlin', lang: 'de-DE' },
];

Use the market's cc in the proxy username and apply tz and lang to each page in that context. See geo-targeted proxies.

For the complete Puppeteer guide, see Puppeteer proxies.

Cleaning up between rotations

Changing the IP is only half of a clean rotation. Anything the browser remembers from the previous identity can link the two together:

  • Cookies and storage. Cookies, localStorage, sessionStorage and IndexedDB persist within a browser context. Closing the context discards them; reusing it carries them across IPs.
  • Service workers and cache. A cached service worker can keep talking to a site in ways you didn't plan for. A fresh context avoids it.
  • Open connections. Chrome keeps connections alive, so requests may keep using the old exit for a while. Closing the context closes its connections.
  • Fingerprint settings. If you set timezone, language or viewport per identity, reset them along with the proxy.

The simplest reliable pattern is one browser context per identity, closed when you rotate:

javascript
const ctx = await browser.createBrowserContext({ proxyServer: 'http://gate.example.com:7777' });
const page = await ctx.newPage();
await page.authenticate({ username: `USER-session-${sessionId}`, password: 'PASS' });
// ... work ...
await ctx.close(); // cookies, storage and connections go with it

For logged-in accounts, do the opposite: keep each account's saved state and IP together, and restore both every time. See using a different proxy per page in Puppeteer, sticky vs rotating sessions and browser fingerprinting and proxies.

Key takeaways

  • In Puppeteer, rotate per browser context (or browser), not per request.
  • With rotating gateways, use a new session ID and a new context for each fresh IP.
  • With static lists, assign one proxy per context and cycle through the list.
  • Detect blocks on content and retry with a fresh session and backoff.
  • Choose context lifetimes per target to balance success and bandwidth. See puppeteer proxy per page.

FAQ

Can I change the proxy of an open page?

Not with Chrome's native proxy settings. Close the context and open a new one.

Why do I keep getting the same IP after changing session ID?

Chrome reused an existing connection. Use a new context or browser for each session.

What's the best proxy type for rotating in Puppeteer?

Rotating residential for protected sites; static datacenter or ISP lists for lenient sites or logged-in flows.

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.