If you are running parsing scripts or multi-accounting on Playwright and launching a separate browser instance for each proxy — you are wasting memory and CPU unnecessarily. Playwright can assign proxies at the BrowserContext level, rather than the entire browser, and this fundamentally changes the architecture of the scripts: one browser process, dozens of isolated contexts with different IPs, cookies, and fingerprints. In this article — how it works, code in Python and Node.js, and why this is especially important when working with residential and mobile proxies.
Problem: Proxies on browser.launch() — expensive and inconvenient
The classic approach of many Playwright scripts looks like this: for each proxy, chromium.launch({ proxy: {...} }) is called, creating a separate browser process, it runs, then closes, and the next one is launched with a new proxy. If you need to handle 50 IP addresses in parallel for parsing Wildberries or Ozon, this means 50 full Chromium processes simultaneously — each consuming 150 to 300 MB of RAM just on startup, not counting tabs and rendering.
In practice, this hits a ceiling already at 15-20 parallel browsers on a regular VPS with 8 GB of memory. The server starts to swap, scripts fail with timeouts, and some proxies do not have time to process. The problem is not with the proxies or Playwright — the problem lies in the architecture: each browser launch carries the entire rendering engine, GPU process, network, and dozens of auxiliary threads, while in fact, you only need a new IP and a clean session.
Playwright elegantly solves this problem: proxies can be set not at browser launch, but when creating a BrowserContext — a lightweight isolated "sandbox" within the same browser process. One Chromium process can hold dozens of contexts, each with its own proxy, cookies, and localStorage, without interfering with each other.
Browser vs BrowserContext: What's the difference
Browser is the engine process itself (Chromium, Firefox, or WebKit), which is launched once and lives long. BrowserContext is an isolated environment within this process, similar to a private window (incognito), with its own cookies, cache, permissions, and, importantly for us, its own network settings, including proxies.
If we compare with anti-detect browsers like Dolphin Anty or AdsPower — each "profile" there is also essentially a separate isolated container with cookies and fingerprints. Playwright allows you to build a similar logic programmatically: one browser — many contexts, each like a separate "profile" with its own IP.
| Parameter | Proxy on Browser | Proxy on BrowserContext |
|---|---|---|
| Memory per IP | 150-300 MB (new process) | 15-40 MB (new context) |
| IP change speed | 2-5 sec (browser restart) | 0.1-0.3 sec (new context) |
| Cookie isolation | Complete (different processes) | Complete (contexts do not share cookies) |
| Max. parallel sessions on 8 GB RAM | ~20-25 | ~150-200 |
Proxy per context: code in Python
Below is a basic pattern: one browser is launched once, and then a separate context is created for each proxy. This is the synchronous API of Playwright, but the logic is identical for the async version.
from playwright.sync_api import sync_playwright
proxies = [
{"server": "http://proxy1.example.com:8000", "username": "user1", "password": "pass1"},
{"server": "http://proxy2.example.com:8000", "username": "user2", "password": "pass2"},
{"server": "http://proxy3.example.com:8000", "username": "user3", "password": "pass3"},
]
with sync_playwright() as p:
# The browser is launched ONCE, without a proxy
browser = p.chromium.launch(headless=True)
for proxy in proxies:
# The proxy is assigned to the context, not the browser
context = browser.new_context(
proxy={
"server": proxy["server"],
"username": proxy["username"],
"password": proxy["password"],
},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
)
page = context.new_page()
page.goto("https://httpbin.org/ip", timeout=30000)
print(page.content())
page.close()
context.close() # only close the context, the browser continues to live
browser.close()
The key point — browser.launch() is called once outside the loop, while browser.new_context() is called inside the proxy loop. For proxies with login and password authentication, Playwright automatically inserts Basic Auth headers at the network level, no additional configuration is needed.
Proxy per context: code in Node.js
The same logic in JavaScript using async/await — often used in conjunction with parsing frameworks on Node.js:
const { chromium } = require('playwright');
const proxies = [
{ server: 'http://proxy1.example.com:8000', username: 'user1', password: 'pass1' },
{ server: 'http://proxy2.example.com:8000', username: 'user2', password: 'pass2' },
];
(async () => {
const browser = await chromium.launch({ headless: true });
for (const proxy of proxies) {
const context = await browser.newContext({
proxy: {
server: proxy.server,
username: proxy.username,
password: proxy.password,
},
});
const page = await context.newPage();
await page.goto('https://httpbin.org/ip', { timeout: 30000 });
console.log(await page.content());
await page.close();
await context.close();
}
await browser.close();
})();
For parallel processing of dozens of proxies, contexts can be created using Promise.all, without waiting for the previous one to close — this is the main advantage of the "one browser, many contexts" approach over launching separate processes.
Clean proxy rotation without restarting the browser
"Clean rotation" means that when changing IP, there are no remnants from the previous session — no cookies, no cache, no service workers. When restarting the entire browser, this is guaranteed automatically, but is resource-intensive. When working with contexts, the same is achieved by closing the old context and creating a new one — the operation takes fractions of a second.
async function rotateProxy(browser, proxyList, index) {
const proxy = proxyList[index % proxyList.length];
const context = await browser.newContext({ proxy });
const page = await context.newPage();
return { context, page };
}
// Use it like this: on every N-th iteration, change IP
for (let i = 0; i < 100; i++) {
if (i % 10 === 0) {
const { context, page } = await rotateProxy(browser, proxies, i);
// work with the new session for 10 requests
}
}
This pattern is especially useful when parsing marketplaces: for example, when collecting prices from Wildberries or Ozon, where bot protection tracks the number of requests from one IP over a unit of time. Context-level rotation allows changing IP every 5-10 requests without losing performance on browser restarts.
For such scenarios, residential proxies are well-suited — they provide IPs of real users, which marketplaces less frequently flag as suspicious, unlike clearly server ranges.
Memory and speed comparison
On a test VPS with 8 GB RAM and 4 vCPUs, a comparison of the two approaches was conducted on the task of bypassing 100 pages with different IPs. In the first case, a separate chromium.launch() was launched for each IP, in the second — one browser with the creation of 100 contexts sequentially.
| Metric | 100 launches of browser.launch() | 100 contexts in 1 browser |
|---|---|---|
| Peak memory | ~6.8 GB | ~1.9 GB |
| Total execution time | 182 sec | 97 sec |
| Number of sessions that timed out | 7 | 0 |
Memory savings in this test amounted to about 70%, and the total execution time was reduced by almost half due to the absence of overhead from launching and terminating the Chromium process each time. At larger volumes — thousands of IPs per day — the difference becomes even more noticeable, and this directly affects how many proxies you can use on one server without upgrading hardware.
Practical cases: parsing and multi-accounting
Parsing marketplaces. When collecting prices from Wildberries, Ozon, or Avito, the same browser keeps dozens of contexts open, each with a unique IP and its own cookie storage. This reduces the risk of blocking due to request patterns and allows collecting data faster, without waiting for the process to restart between IPs.
Multi-accounting and account farming. For testing advertising campaigns in Facebook Ads or TikTok Ads through Playwright scripts, it makes sense to keep each account in a separate context with its own proxy and user-agent — this simulates the behavior of anti-detect browsers like Multilogin or GoLogin, only without a license for the anti-detect itself, fully programmatically.
Geographic testing of ads. Marketers who need to check how a creative appears in different countries can use contexts with different proxies to "visit" the required regions in one browser session, without manually opening dozens of separate windows.
For scenarios where it is important to simulate mobile traffic — for example, checking the mobile version of a website or ads in TikTok Ads — consider mobile proxies, which provide IPs from real mobile operators and are less likely to raise suspicion with anti-fraud systems.
If the task is fast and stable parsing without the need for high anonymity (for example, collecting public data from sites without strict anti-bot protection), data center proxies offer the best balance of speed and cost at high volumes of requests.
Common mistakes and how to avoid them
1. Proxies are set in launch(), not in new_context(). This is the most common mistake among beginners — a proxy specified when launching the browser applies to all contexts at once and cannot be overridden individually. Always pass the proxy in new_context() if you need flexibility with IPs.
2. Old contexts are not closed. If you create hundreds of contexts and do not call context.close(), the browser's memory will grow linearly until the process crashes due to OOM. Always close the context after completing the task for that IP.
3. One user-agent for all contexts. If the proxy changes, but the browser's fingerprint remains the same (user-agent, language, timezone), anti-bot systems of marketplaces and social networks may still link sessions together. Change the user-agent and viewport along with the proxy for each context.
4. Ignoring proxy timeouts. Residential and mobile proxies sometimes respond slower than data center proxies due to real network infrastructure. Set a reasonable timeout (20-40 seconds) when calling page.goto(), otherwise the script will consider a working proxy "dead."
5. Too many contexts in one browser simultaneously. Despite memory savings, uncontrolled creation of 500+ active contexts simultaneously will still hit process limits. A reasonable balance is 30-80 simultaneous active contexts per browser process, depending on the server's RAM capacity.
Conclusion
Assigning proxies at the BrowserContext level, rather than to the entire browser — is an architectural solution that saves up to 70% of memory, speeds up IP rotation by dozens of times, and provides clean cookie isolation between sessions without constant restarts of Chromium. For parsing marketplaces, testing ads in different regions, and multi-accounting, this is a direct way to increase the number of parallel sessions on the same server without upgrading hardware.
If you are building a Playwright script for collecting data from Wildberries, Ozon, or for working with advertising accounts in Facebook Ads and TikTok Ads, the quality of the proxy pool is just as important as the architecture of the code. For tasks where the reliability of IP and resistance to anti-bot systems are critical, it is worth starting with residential proxies — they pair well with the "one browser, many contexts" approach described above.