Playwright & Web Automation Hub

Playwright architecture, selector reliability, and advanced interaction patterns.

How to Configure Multiple Browser Contexts in Playwright

Implementing parallel session isolation requires precise control over browser lifecycle management. This guide details the exact configuration workflow for spawning, isolating, and managing concurrent contexts. You will eliminate state leakage while minimizing memory overhead in CI/CD pipelines.

Core Architecture & Isolation Principles

Playwright contexts operate as lightweight alternatives to separate browser instances. Each context maintains an independent session within a single browser process. This architecture guarantees strict cookie jar separation and scoped localStorage. Execution threads run concurrently without cross-contamination. This design aligns directly with established Browser Contexts & Isolation standards.

Context vs. Incognito: Performance & Memory Footprint

Traditional incognito windows spawn entirely new OS-level processes. Contexts bypass this overhead by sharing the underlying renderer engine. CPU and memory consumption remain stable when scaling to dozens of concurrent sessions. This efficiency is critical for large-scale data extraction and parallel test execution.

Step-by-Step Configuration Workflow

Deterministic automation pipelines require strict initialization sequencing. Follow this workflow to align with foundational Playwright Setup & Core Architecture patterns.

1. Launch Browser Instance

Initialize the headless browser using chromium.launch(). Configure headless: true and pass stability flags via args if required by your CI environment. Never instantiate pages directly on the browser object. Always await the launch promise before proceeding to context creation.

2. Spawn Independent Contexts

Invoke browser.newContext() for each isolated session. Inject distinct storageState, viewport, and locale parameters to simulate varied user environments. Chain explicit await operators to prevent race conditions during initialization. Each context must be treated as a standalone execution boundary.

Minimal Reproducible Example

The following implementation demonstrates concurrent execution with explicit waits and deterministic cleanup. It enforces modern async/await patterns and avoids deprecated navigation listeners.

const { chromium } = require('playwright');

(async () => {
 const browser = await chromium.launch({ headless: true });
 
 // Context A: Authenticated Session
 const contextA = await browser.newContext({
 storageState: 'auth-state.json',
 viewport: { width: 1280, height: 720 }
 });
 const pageA = await contextA.newPage();
 
 // Context B: Guest/Incognito Session
 const contextB = await browser.newContext();
 const pageB = await contextB.newPage();

 // Execute concurrently with explicit waits
 const [resA, resB] = await Promise.all([
 (async () => {
 await pageA.goto('https://app.example.com/dashboard');
 await pageA.waitForSelector('[data-testid="user-profile"]', { state: 'visible', timeout: 10000 });
 return await pageA.title();
 })(),
 (async () => {
 await pageB.goto('https://app.example.com/login');
 await pageB.waitForURL('**/login', { timeout: 15000 });
 return await pageB.title();
 })()
 ]);

 console.log('Context A:', resA);
 console.log('Context B:', resB);

 // Explicit cleanup to prevent memory leaks
 await contextA.close();
 await contextB.close();
 await browser.close();
})();

Handling Shared State & Race Conditions

Concurrent workflows require strict synchronization boundaries. Never share mutable state objects across context boundaries. Use Promise.all() to orchestrate parallel page interactions while maintaining independent execution paths. Rely exclusively on explicit waitForSelector and waitForURL conditions. These guarantees prevent flaky assertions during rapid DOM updates.

When injecting API tokens or session cookies, serialize the state into JSON before context creation. Deserialize it directly into storageState during initialization. This approach eliminates runtime token refresh conflicts. It ensures deterministic authentication across parallel workers.

Troubleshooting Context Limits & Memory Leaks

Target closed errors typically indicate premature context disposal or unhandled promise rejections. Always sequence context.close() calls before invoking browser.close(). Failing to close contexts explicitly triggers memory leaks in long-running Node.js processes.

Monitor context pool exhaustion by tracking active handles in your test runner. Implement try...finally blocks to guarantee cleanup execution even when assertions fail. Enable garbage collection hints in your CI runner to reclaim renderer memory between parallel suites. Strict lifecycle management ensures stable throughput under heavy load.

Back to overview