The problem
Chrome's --proxy-server flag applies to the whole browser. Every tab uses the same proxy. But many workflows need different proxies at the same time: scraping several countries in parallel, running multiple accounts, or isolating sessions. There are three ways to do it in Puppeteer, each with trade-offs.
Option 1: Per-context proxies (recommended)
Puppeteer's browser contexts are isolated, incognito-like profiles within one browser process. Recent Puppeteer versions accept a proxyServer option when creating a context, so each context, and every page inside it, can use its own proxy:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
async function pageWithProxy({ server, username, password }) {
const ctx = await browser.createBrowserContext({ proxyServer: server });
const page = await ctx.newPage();
if (username) await page.authenticate({ username, password });
return { ctx, page };
}
const us = await pageWithProxy({ server: 'http://gate.example.com:7777', username: 'customer-USER-country-us', password: 'PASS' });
const de = await pageWithProxy({ server: 'http://gate.example.com:7777', username: 'customer-USER-country-de', password: 'PASS' });
await Promise.all([us.page.goto('https://httpbin.org/ip'), de.page.goto('https://httpbin.org/ip')]);
console.log(await us.page.evaluate(() => document.body.innerText));
console.log(await de.page.evaluate(() => document.body.innerText));
await us.ctx.close();
await de.ctx.close();
await browser.close();Note: in older Puppeteer releases the method was browser.createIncognitoBrowserContext(). Check your version.
Pros: one browser process, isolated cookies and storage per context, native Chrome networking (genuine TLS fingerprint). Cons: contexts share the browser's fingerprint (same user agent and hardware signals) unless you override them per page.
Option 2: One browser per proxy
Launch a separate browser for each proxy:
async function browserWithProxy(server) {
return puppeteer.launch({ args: [`--proxy-server=${server}`] });
}Pros: complete isolation, simple mental model, works on any Puppeteer version. Cons: each browser uses significant RAM and CPU; doesn't scale to many concurrent proxies on small servers.
Option 3: Request interception libraries
Packages such as puppeteer-page-proxy intercept each request in Node.js and re-send it through a chosen proxy using an HTTP library, then feed the response back to the page.
Pros: per-page or even per-request proxies within one context. Cons: requests no longer go through Chrome's network stack, so the TLS and HTTP/2 fingerprint becomes Node's, not Chrome's, which bot protection can detect. Also adds overhead and can break some resources. Use only on lenient sites.
Comparison
| Approach | Isolation | Resource use | Fingerprint fidelity | Best for |
|---|---|---|---|---|
| Per-context proxy | Cookies/storage per context | Low | Native Chrome | Most use cases |
| Browser per proxy | Full | High | Native Chrome | Few proxies, max isolation |
| Request interception | Per page/request | Medium | Altered | Lenient targets only |
Multiple accounts
For managing multiple accounts, per-context proxies give each account its own IP and cookie jar, but contexts share the underlying browser fingerprint. For account work on strict platforms, an antidetect browser or separate browser profiles with distinct fingerprints is safer.
Overriding fingerprints per context
When contexts share a browser, they share the same underlying user agent and hardware signals unless you override them. For per-context variety:
const page = await ctx.newPage();
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0 Safari/537.36');
await page.setViewport({ width: 1440, height: 900 });
await page.emulateTimezone('America/Los_Angeles');
await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US,en;q=0.9' });Keep overrides internally consistent: a macOS user agent with Windows-specific signals elsewhere is a giveaway. See browser fingerprinting and proxies.
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.
A context manager pattern
async function withProxyPage(browser, proxy, fn) {
const ctx = await browser.createBrowserContext({ proxyServer: proxy.server });
try {
const page = await ctx.newPage();
if (proxy.username) await page.authenticate({ username: proxy.username, password: proxy.password });
return await fn(page);
} finally {
await ctx.close();
}
}
const html = await withProxyPage(browser, { server: 'http://gate.example.com:7777', username: 'u-session-1', password: 'p' },
async (page) => { await page.goto('https://example.com'); return page.content(); });This guarantees contexts are closed even when navigation throws.
Choosing the right approach quickly
- Need different IPs at the same time? Per-context proxies.
- Need distinct fingerprints too? Separate browsers or an antidetect browser.
- On an old Puppeteer version? Browser per proxy, or upgrade.
- Lenient target, many proxies, low resources? Request interception may be acceptable.
For rotation patterns, see Puppeteer rotating proxies; for scaling, Puppeteer cluster proxies.
Matching each context to its proxy's location
When contexts use proxies in different countries, align each page's locale settings with the exit. A simple approach is to look up the exit location once per context:
async function localiseToExit(page) {
await page.goto('https://ipinfo.io/json', { waitUntil: 'domcontentloaded' });
const info = JSON.parse(await page.evaluate(() => document.body.innerText));
await page.emulateTimezone(info.timezone);
const lang = { US: 'en-US', GB: 'en-GB', DE: 'de-DE', FR: 'fr-FR' }[info.country] || 'en-US';
await page.setExtraHTTPHeaders({ 'Accept-Language': `${lang},en;q=0.8` });
return info;
}Call it after authentication and before visiting your target. This avoids the classic mismatch of a German IP with a US timezone. See geo-targeted proxies and browser fingerprinting and proxies.
Resource limits with many contexts
| Concurrent contexts | Typical server need |
|---|---|
| 5 to 10 | Small VPS |
| 20 to 50 | Several CPU cores, several GB RAM |
| 100+ | Multiple machines or a browser grid |
Heavy pages (video, large SPAs) need more. Block images and media to reduce both bandwidth and memory, and close contexts promptly.
Common errors with per-context proxies
proxyServerignored: your Puppeteer version predates the option; upgrade or use one browser per proxy.- Auth prompts per page: remember
page.authenticate()for each new page in the context. - Same IP across contexts with a rotating gateway: each context needs a different session ID in the username.
For the full set of Puppeteer techniques, see the Puppeteer proxies pillar guide.
Per-context proxies and the bypass list
Along with proxyServer, Puppeteer's BrowserContextOptions accepts a proxyBypassList: hosts that skip the proxy for that context.
const ctx = await browser.createBrowserContext({
proxyServer: 'http://gate.example.com:7777',
proxyBypassList: ['localhost', '127.0.0.1', '*.internal.example.com'],
});Useful cases:
- Local services. A mock server, a local forwarder or a test app on
localhostshouldn't go through a paid proxy. - Your own infrastructure. Internal APIs or asset hosts you control can go direct, saving bandwidth.
- Debugging. Bypassing an echo service temporarily lets you compare direct and proxied responses in the same context.
The catch: every bypassed host sees your real server IP, and so does anything it reports to. Never bypass third-party domains that belong to, or report to, the site you're automating, such as its CDN, analytics or bot detection scripts. A page that loads half its resources through a residential IP and half from a datacenter IP is easy to spot.
When in doubt, keep the bypass list to localhost and your own domains. For blocking resources you don't need at all, which saves more bandwidth than bypassing, see Puppeteer proxies. For DNS behaviour with per-context proxies, see DNS leaks and proxies.
Key takeaways
- Chrome's proxy flag is browser-wide, so per-page proxies need contexts, separate browsers or interception.
- Per-context
proxyServerin recent Puppeteer is the best balance of isolation and efficiency. - Request interception libraries change your TLS fingerprint and suit only lenient targets.
- Localise each context to its exit (timezone, language).
- Close contexts promptly and watch memory at scale. See puppeteer cluster proxies.
FAQ
Can I set a proxy per tab in Chrome?
Not natively. Use separate contexts or browsers.
Does per-context proxy work with SOCKS5?
Yes, with socks5:// in proxyServer, but without username/password. See Puppeteer SOCKS5 proxy.
How many contexts can one browser handle?
Dozens on a decent server, depending on page weight. Monitor memory.
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.