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:
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:
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:
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?
| Workload | Rotate |
|---|---|
| Independent product or listing pages | New context per page (or per few pages) |
| Paginated search | One context for the pagination run |
| Logged-in session | Don't rotate; use a static ISP IP |
| Challenge-protected sites | Keep 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:
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 behaviour | Context lifetime |
|---|---|
| Tolerant, no challenges | Reuse a context for many pages, rotate periodically |
| Challenge on first visit, cookie-based clearance | Keep context while clearance lasts |
| Strict per-IP limits | Rotate every few pages |
| Logged-in flows | One 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:
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,sessionStorageand 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:
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 itFor 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.
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.