Per-Context Proxy: Independent Network Identity for Every Browser Context
Configure independent proxy and geographic identity per BrowserContext. Run multiple regions in a single browser instance with automatic timezone, locale, and language alignment.
Introduction
When running multiple identities or regional workflows in a browser, a single proxy for the entire session is not enough. If every context shares the same IP address, traffic from different identities can be linked by that shared network path. Tracking systems observe IP patterns, and two accounts originating from the same address at similar times are easy to correlate.
Per-context proxy solves this by assigning a dedicated proxy to each BrowserContext. Each context routes through a different proxy server, receives its own public IP, and gets geographic metadata (timezone, locale, language) derived from that proxy's exit location. Combined with per-context fingerprint isolation, each context becomes a fully independent identity with no shared network or fingerprint signals.
This article covers how per-context proxy works in BotBrowser, how to configure it in both Puppeteer and Playwright, and how to optimize performance for multi-region deployments.
Privacy Impact
A single proxy creates a shared network identity across all contexts. Even with different fingerprint profiles, the shared IP becomes a correlation point. Consider these risks:
- IP-based correlation: Two accounts accessing the same service from the same IP can be linked, regardless of fingerprint differences
- Geographic inconsistency: A context configured with a German fingerprint profile but routed through a US proxy creates an obvious mismatch between stated location and network origin
- Timing analysis: Multiple identities sharing one proxy can be correlated by their traffic timing patterns, since all requests originate from the same network endpoint
Per-context proxy eliminates these risks. Each context has its own proxy, its own public IP, and its own geographic metadata. There is no shared network signal between contexts.
Technical Background
BrowserContext-Level Network Isolation
In Chromium, a BrowserContext is an isolated browsing environment. Each context has its own cookie jar, localStorage, sessionStorage, IndexedDB, and cache. Standard Chromium provides this storage isolation out of the box.
BotBrowser extends this isolation to the network layer. When a BrowserContext is assigned its own proxy, all HTTP, HTTPS, and WebSocket traffic from that context routes through the specified proxy server. Other contexts in the same browser instance are unaffected. This is true network isolation at the context level, not a page-level interceptor that can miss early requests or WebSocket connections.
Automatic Geographic Detection
When a context connects through a proxy, BotBrowser detects the proxy's exit IP and automatically derives geographic settings for that specific context:
- Timezone: Derived from the proxy IP's geolocation (e.g.,
America/New_Yorkfor a US East proxy) - Locale: Matched to the proxy's country (e.g.,
en-US) - Languages: Set based on the proxy's region (e.g.,
en-US,en) - Geolocation: Coordinates approximated from the IP
This happens independently for each context. A US proxy context gets US geographic settings, while a German proxy context in the same browser gets German settings. No manual configuration of timezone, locale, or language is needed.
Integration with Per-Context Fingerprint
Per-context proxy works alongside BotBrowser's per-context fingerprint isolation (ENT Tier3). Each context can receive:
- A unique fingerprint profile via
--bot-profile - An independent proxy and public IP
- Matching geographic metadata derived from the proxy
- Isolated storage (cookies, localStorage, IndexedDB)
The combination means each context is a fully independent identity. No fingerprint signals, network paths, or storage are shared between contexts.
Configuration
Puppeteer: Multi-Region Setup
In Puppeteer, per-context proxy is configured through CDP (Chrome DevTools Protocol). You create a BrowserContext, assign proxy settings and a fingerprint profile via BotBrowser.setBrowserContextFlags, then create pages within that context.
const puppeteer = require('puppeteer-core');
const browser = await puppeteer.launch({
executablePath: process.env.BOTBROWSER_EXEC_PATH,
headless: true,
defaultViewport: null,
args: ['--bot-profile=/path/to/default-profile.enc'],
});
const client = await browser.target().createCDPSession();
// US context with US proxy
const usCtx = await browser.createBrowserContext({
proxyServer: 'socks5://user:pass@us-proxy.example.com:1080',
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: usCtx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/us-profile.enc',
'--proxy-ip=203.0.113.1',
],
});
const usPage = await usCtx.newPage();
// UK context with UK proxy
const ukCtx = await browser.createBrowserContext({
proxyServer: 'socks5://user:pass@uk-proxy.example.com:1080',
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ukCtx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/uk-profile.enc',
'--proxy-ip=198.51.100.1',
],
});
const ukPage = await ukCtx.newPage();
// DE context with German proxy
const deCtx = await browser.createBrowserContext({
proxyServer: 'socks5://user:pass@de-proxy.example.com:1080',
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: deCtx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/de-profile.enc',
'--proxy-ip=192.0.2.1',
],
});
const dePage = await deCtx.newPage();
// Each context navigates with its own proxy and geographic identity
await Promise.all([
usPage.goto('https://example.com'),
ukPage.goto('https://example.com'),
dePage.goto('https://example.com'),
]);
await browser.close();
Important: BotBrowser.setBrowserContextFlags must be called before creating any page in that context. The renderer process reads its flags at startup. If a page already exists, the flags will not take effect.
Puppeteer: Proxy via botbrowserFlags Only
You can also configure the proxy entirely through botbrowserFlags, without passing proxyServer to createBrowserContext:
const ctx = await browser.createBrowserContext();
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/profile.enc',
'--proxy-server=socks5://user:pass@proxy.example.com:1080',
'--proxy-ip=203.0.113.1',
'--proxy-bypass-list=localhost;127.0.0.1',
],
});
const page = await ctx.newPage();
This approach configures all proxy parameters (server, IP, bypass rules) in a single place.
Playwright: Per-Context Proxy
Playwright provides native per-context proxy support through browser.newContext():
const { chromium } = require('playwright-core');
const browser = await chromium.launch({
executablePath: '/path/to/botbrowser/chrome',
args: ['--bot-profile=/path/to/profile.enc'],
headless: true,
});
// US context
const usContext = await browser.newContext({
proxy: { server: 'socks5://us-proxy:1080', username: 'user', password: 'pass' },
});
// Germany context
const deContext = await browser.newContext({
proxy: { server: 'socks5://de-proxy:1080', username: 'user', password: 'pass' },
});
// Each context automatically gets geographic settings matching its proxy
const usPage = await usContext.newPage();
const dePage = await deContext.newPage();
await usPage.goto('https://example.com');
await dePage.goto('https://example.com');
BotBrowser auto-derives timezone, locale, and language for each context based on its proxy's exit IP.
Skipping IP Detection with --proxy-ip
Each context's proxy triggers an IP detection request to determine geographic settings. If you already know the proxy's exit IP, you can skip this detection step with --proxy-ip:
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/profile.enc',
'--proxy-server=socks5://user:pass@proxy.example.com:1080',
'--proxy-ip=203.0.113.1', // Skip IP lookup, use this IP for geo detection
],
});
This eliminates the per-context IP lookup latency, which is especially beneficial when creating many contexts in sequence.
Selective Routing with proxy-bypass-rgx
Use --proxy-bypass-rgx to route specific URLs directly instead of through the proxy. This reduces proxy bandwidth on static assets or internal services:
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/profile.enc',
'--proxy-server=socks5://user:pass@proxy.example.com:1080',
'--proxy-bypass-list=localhost;127.0.0.1',
'--proxy-bypass-rgx=\\.(js|css|png|jpg|svg)(\\?|$)',
],
});
The --proxy-bypass-list uses the standard Chromium semicolon-separated host list. The --proxy-bypass-rgx uses RE2 regex syntax and matches against both hostname and URL path.
Common Scenarios
Multi-Region Testing in One Browser
Run US, UK, and German contexts simultaneously within a single browser instance. Each context gets a different proxy, fingerprint profile, and geographic identity:
const regions = [
{
name: 'US',
proxy: 'socks5://user:pass@us.proxy.example.com:1080',
ip: '203.0.113.1',
profile: '/path/to/us-profile.enc',
},
{
name: 'UK',
proxy: 'socks5://user:pass@uk.proxy.example.com:1080',
ip: '198.51.100.1',
profile: '/path/to/uk-profile.enc',
},
{
name: 'DE',
proxy: 'socks5://user:pass@de.proxy.example.com:1080',
ip: '192.0.2.1',
profile: '/path/to/de-profile.enc',
},
];
const client = await browser.target().createCDPSession();
for (const region of regions) {
const ctx = await browser.createBrowserContext({
proxyServer: region.proxy,
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
`--bot-profile=${region.profile}`,
`--proxy-ip=${region.ip}`,
],
});
const page = await ctx.newPage();
await page.goto('https://example.com');
// Verify geographic identity
const tz = await page.evaluate(() =>
Intl.DateTimeFormat().resolvedOptions().timeZone
);
const lang = await page.evaluate(() => navigator.language);
console.log(`${region.name}: timezone=${tz}, language=${lang}`);
}
Per-Context: Different Profile + Different Proxy
Each context can use a completely different fingerprint profile. A Windows profile with a US proxy and a macOS profile with a UK proxy, all within one browser:
// Windows identity with US proxy
const winCtx = await browser.createBrowserContext({
proxyServer: 'socks5://user:pass@us-proxy.example.com:1080',
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: winCtx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/windows-profile.enc',
'--proxy-ip=203.0.113.1',
],
});
// macOS identity with UK proxy
const macCtx = await browser.createBrowserContext({
proxyServer: 'socks5://user:pass@uk-proxy.example.com:1080',
});
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: macCtx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/macos-profile.enc',
'--proxy-ip=198.51.100.1',
],
});
Runtime Proxy Switching
For scenarios that require changing a context's proxy after creation (e.g., geo-rotation within a session), use BotBrowser.setBrowserContextProxy (ENT Tier3):
const ctx = await browser.createBrowserContext();
const page = await ctx.newPage();
const client = await page.createCDPSession();
// Start with US proxy
await client.send('BotBrowser.setBrowserContextProxy', {
browserContextId: ctx._contextId,
proxyServer: 'socks5://user:pass@us-proxy.example.com:1080',
proxyIp: '203.0.113.1',
});
await page.goto('https://example.com');
// Switch to UK proxy at runtime
await client.send('BotBrowser.setBrowserContextProxy', {
browserContextId: ctx._contextId,
proxyServer: 'socks5h://user:pass@uk-proxy.example.com:1080',
proxyIp: '198.51.100.1',
proxyBypassList: 'localhost;127.0.0.1',
proxyBypassRgx: 'cdn\\.example\\.com|/static/',
});
await page.goto('https://example.co.uk');
// Switch to Japan proxy
await client.send('BotBrowser.setBrowserContextProxy', {
browserContextId: ctx._contextId,
proxyServer: 'socks5://user:pass@jp-proxy.example.com:1080',
proxyIp: '192.0.2.1',
});
await page.goto('https://example.jp');
After each switch, BotBrowser re-detects geographic settings and applies them to the context. The geo update takes effect on the next main-frame navigation.
Performance Optimization
Use --proxy-ip to Reduce Lookup Overhead
When you know the exit IP for each proxy, always pass --proxy-ip. Without it, BotBrowser performs an IP detection request for each context on the first navigation. With 10+ contexts, these lookups add up:
// Without --proxy-ip: each context makes an IP detection request
// With --proxy-ip: geo detection is instant, no network request needed
const contexts = proxies.map(async (proxy) => {
const ctx = await browser.createBrowserContext({ proxyServer: proxy.server });
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
'--bot-profile=/path/to/profile.enc',
`--proxy-ip=${proxy.knownIp}`,
],
});
return ctx;
});
Shared Processes Save Resources
Per-context proxy within a single browser instance shares the GPU process, browser process, and utility processes. Compared to launching separate browser instances for each proxy, this approach saves:
| Resource | Separate Instances (10 proxies) | Per-Context (10 contexts) |
|---|---|---|
| Browser processes | 10 | 1 |
| GPU processes | 10 | 1 |
| Network processes | 10 | 1 |
| Base memory overhead | ~500 MB | ~50 MB |
| Context creation time | 1-3 seconds each | Milliseconds each |
Combine with Per-Context Fingerprint
When you assign both a proxy and a fingerprint profile per context, each context becomes a fully independent identity without the overhead of separate browser instances:
// Single browser instance, 3 complete identities
const identities = [
{ profile: '/profiles/win-us.enc', proxy: 'socks5://us:1080', ip: '203.0.113.1' },
{ profile: '/profiles/mac-uk.enc', proxy: 'socks5://uk:1080', ip: '198.51.100.1' },
{ profile: '/profiles/linux-de.enc', proxy: 'socks5://de:1080', ip: '192.0.2.1' },
];
for (const id of identities) {
const ctx = await browser.createBrowserContext({ proxyServer: id.proxy });
await client.send('BotBrowser.setBrowserContextFlags', {
browserContextId: ctx._contextId,
botbrowserFlags: [
`--bot-profile=${id.profile}`,
`--proxy-ip=${id.ip}`,
],
});
// Each context: unique fingerprint + unique proxy + unique geo = fully independent
}
Verification
After setting up per-context proxies, verify each context independently:
async function verifyContext(context, label) {
const page = await context.newPage();
// Check public IP
await page.goto('https://httpbin.org/ip');
const ipData = await page.evaluate(() => document.body.textContent);
console.log(`[${label}] IP: ${ipData.trim()}`);
// Check timezone
const tz = await page.evaluate(() =>
Intl.DateTimeFormat().resolvedOptions().timeZone
);
console.log(`[${label}] Timezone: ${tz}`);
// Check language
const lang = await page.evaluate(() => navigator.language);
console.log(`[${label}] Language: ${lang}`);
// Check locale
const locale = await page.evaluate(() =>
Intl.NumberFormat().resolvedOptions().locale
);
console.log(`[${label}] Locale: ${locale}`);
await page.close();
}
await verifyContext(usCtx, 'US');
await verifyContext(ukCtx, 'UK');
await verifyContext(deCtx, 'DE');
Confirm that each context shows a different IP, timezone, and locale matching the proxy's geographic location.
Frequently Asked Questions
What license tier is required for per-context proxy?
Per-context proxy with fingerprint isolation requires ENT Tier3. Runtime proxy switching via BotBrowser.setBrowserContextProxy also requires ENT Tier3.
Can I use Playwright for per-context proxy?
Yes. Playwright natively supports per-context proxy through browser.newContext({ proxy: ... }). BotBrowser auto-derives geographic settings for each Playwright context.
Must setBrowserContextFlags be called before creating a page?
Yes. The renderer process reads its flags at startup. If a page already exists in the context, the new flags will not apply. The correct order is: createBrowserContext -> setBrowserContextFlags -> newPage.
Can I change a context's proxy after creation?
Yes, via BotBrowser.setBrowserContextProxy (ENT Tier3). This allows runtime proxy switching without recreating the context. Geographic settings are re-derived on the next main-frame navigation.
What proxy protocols are supported?
All protocols supported by --proxy-server work per-context: socks5://, socks5h://, http://, https://. All support embedded authentication (user:pass@host:port).
How many contexts can run simultaneously? There is no hard limit. Each context consumes memory proportional to its open pages and cached resources. In practice, dozens or hundreds of contexts can run within a single browser instance with sufficient RAM.
Does BotBrowser auto-detect geography for each context? Yes. Each context with a different proxy gets independent geo-detection. BotBrowser detects the exit IP and configures timezone, locale, and language for that specific context.
What happens if I do not set --proxy-ip?
BotBrowser performs an automatic IP detection request on the first navigation for each context. This works correctly but adds a small latency overhead. Setting --proxy-ip eliminates this lookup.
Can I use different bypass rules per context?
Yes. Both --proxy-bypass-list and --proxy-bypass-rgx can be set per context through botbrowserFlags.
Summary
Per-context proxy provides complete network isolation for each BrowserContext within a single browser instance. Each context routes through its own proxy, receives an independent public IP, and gets geographic metadata (timezone, locale, language) automatically derived from the proxy's exit location. Combined with per-context fingerprint profiles, every context operates as a fully independent identity.
For proxy fundamentals and protocol details, see Proxy Configuration. For runtime proxy switching within existing contexts, see Dynamic Proxy Switching. For complete multi-identity isolation, see Multi-Account Browser Isolation. For DNS and WebRTC leak protection across all contexts, combine with DNS Leak Prevention and WebRTC Leak Prevention.