How to Use Browser Context APIs for Isolated Sessions
Learn how browser contexts isolate cookies, storage, permissions and users in Playwright and Puppeteer, with runnable patterns and troubleshooting.
Short answer: create one browser context for each independent test or user, then create pages inside that context. A context isolates cookies, local storage and session storage while sharing the underlying browser process. Close each context when its work is complete.
In Playwright, the lifecycle is browser.newContext() → context.newPage() → navigation and actions → context.close() → browser.close(). Playwright describes contexts as isolated, incognito-like profiles that are fast and inexpensive to create. See the Playwright isolation guide and BrowserContext API reference.
Browser context versus browser versus page
| Object | What it represents | What is isolated |
|---|---|---|
| Browser | The launched Chromium, Firefox or WebKit instance | Process-level resources and browser engines |
| BrowserContext | An independent session container inside a browser | Cookies, local storage, session storage, permissions and context-level routes |
| Page | A tab-like document opened inside a context | It shares its parent context’s session state |
A context is not a separate operating-system browser process. Multiple contexts can run in one browser while remaining independent. A context can contain multiple pages, and those pages intentionally share that context’s state.
Install Playwright
npm install -D playwright
npx playwright install
The browser binaries are installed separately from the Node.js package. In CI, run the install command during image or job setup.
Create a clean isolated session
This complete script creates a non-persistent context, visits a page, takes a screenshot and closes resources in the correct order.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
try {
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.screenshot({ path: 'example.png', fullPage: true });
} finally {
await context.close();
await browser.close();
}
})();
Non-persistent contexts do not write browsing data to disk. Create a new one for every independent test or workflow so a previous login, cart, consent choice or visited-link state cannot leak into the next scenario.
Run two users in one browser
Create separate contexts for each identity. Each user can open several pages in its own context, but neither user sees the other’s cookies or storage.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const adminContext = await browser.newContext();
const userContext = await browser.newContext();
try {
const adminPage = await adminContext.newPage();
const userPage = await userContext.newPage();
await adminPage.goto('https://app.example.test/admin');
await userPage.goto('https://app.example.test/dashboard');
// Perform an approval as admin and verify it as the regular user.
await adminPage.getByRole('button', { name: 'Approve' }).click();
await userPage.reload();
await userPage.getByText('Approved').waitFor();
} finally {
await adminContext.close();
await userContext.close();
await browser.close();
}
})();
This pattern fits chat tests, permission changes, approval workflows and any scenario where two independent identities must interact.
Configure isolation at the context boundary
Set session-wide behavior when creating the context. Common options include:
| Option | Use |
|---|---|
viewport |
Choose the virtual screen size. |
userAgent |
Run a session with a specific user-agent string. |
locale |
Set language and locale-sensitive formatting. |
timezoneId |
Test date and time behavior in another timezone. |
geolocation and permissions |
Provide a location and grant location-related permissions. |
colorScheme |
Emulate light or dark mode. |
extraHTTPHeaders |
Add headers to requests from pages in the context. |
storageState |
Start with saved cookies and origin storage. |
serviceWorkers |
Control service-worker behavior when debugging network interactions. |
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
locale: 'en-US',
timezoneId: 'America/New_York',
colorScheme: 'dark',
userAgent: 'isolated-test-runner/1.0',
extraHTTPHeaders: {
'x-test-run': 'checkout-case'
},
geolocation: { latitude: 40.7128, longitude: -74.0060 },
permissions: ['geolocation']
});
Keep credentials and other secrets out of source control. Prefer environment variables or your CI secret store when supplying headers, cookies or storage files.
Reuse sign-in safely with storage state
storageState() can save cookies, local storage, IndexedDB and other supported origin data. Save it once through a controlled login, then create a fresh context from that snapshot for each test.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const loginContext = await browser.newContext();
const loginPage = await loginContext.newPage();
await loginPage.goto('https://app.example.test/login');
await loginPage.getByLabel('Email').fill(process.env.TEST_EMAIL);
await loginPage.getByLabel('Password').fill(process.env.TEST_PASSWORD);
await loginPage.getByRole('button', { name: 'Sign in' }).click();
await loginPage.waitForURL('**/dashboard');
await loginContext.storageState({ path: 'playwright/.auth/user.json' });
await loginContext.close();
const testContext = await browser.newContext({
storageState: 'playwright/.auth/user.json'
});
try {
const page = await testContext.newPage();
await page.goto('https://app.example.test/dashboard');
} finally {
await testContext.close();
await browser.close();
}
})();
Treat the saved file as a credential. Do not commit it, publish it as an artifact, or reuse it across tests that require different identities.
Cookies, permissions and network routing
Context methods apply to every page in that context. This makes setup predictable when a workflow opens popups or additional tabs.
const context = await browser.newContext();
await context.addCookies([{
name: 'feature_flag',
value: 'new-checkout',
domain: 'app.example.test',
path: '/'
}]);
await context.grantPermissions(['notifications'], {
origin: 'https://app.example.test'
});
await context.route('**/*.{png,jpg,jpeg,gif}', route => route.abort());
const page = await context.newPage();
await page.goto('https://app.example.test');
Install routes before navigation so the first document and its subresources are covered. Remove or close the context when the route is no longer needed.
Pages share a context; contexts do not share state
const firstTab = await context.newPage();
const secondTab = await context.newPage();
// These pages share cookies and local storage.
await firstTab.goto('https://app.example.test');
await secondTab.goto('https://app.example.test/account');
Use another context when you need a separate login or storage namespace. Opening another page is not an isolation boundary.
Playwright and Puppeteer terminology
Puppeteer also exposes BrowserContext; its documentation describes isolated storage such as cookies and local storage, with non-default Chrome contexts operating as incognito contexts. The concept is the same, but method names and lifecycle details follow the framework version you installed. Consult the Puppeteer BrowserContext reference.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
} finally {
await context.close();
await browser.close();
}
})();
Edge cases to plan for
- Persistent contexts: a persistent profile intentionally writes user data to a directory. Use one only when disk-backed state is part of the scenario; use non-persistent contexts for clean tests.
- Popups and new tabs: pages created with
context.waitForEvent('page')remain in the same context and share its state. - Service workers: cached responses can make a page appear to retain state. Clear the relevant data or configure service-worker behavior when investigating cache issues.
- Parallel tests: give each worker its own context and unique test data. Isolation does not prevent two users from changing the same server-side record.
- Visited links and server data: a fresh context resets browser state, but it cannot undo changes already stored by the application backend.
- Downloads, videos and HAR files: close the context before the browser so context-owned artifacts can be flushed.
- Authentication expiry: a storage snapshot can become invalid. Detect redirects to login and regenerate the snapshot instead of silently continuing as an anonymous user.
Or skip the browser setup
If your goal is a clean screenshot rather than interactive browser testing, ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP or PDF. Cookie and consent banners, newsletter popups and chat widgets are removed before capture; bot checks, blank pages and failed loads are not billed.
See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Each response reports its result through X-Page-Verdict and X-Billed headers. Cache hits and unsuccessful captures cost nothing. ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account to get started.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| A test is already logged in | The same context or storage snapshot is reused. | Create a new context per scenario and verify the snapshot belongs to the intended user. |
| Two users see the same account | Both pages were created in one context, or both use the same cookies. | Create two contexts and authenticate each independently. |
| State disappears unexpectedly | The context was closed, or a non-persistent context was mistaken for a disk profile. | Keep the context alive for the workflow; save and load storage state when reuse is intentional. |
| Headers or routes do not affect the first request | They were installed after navigation. | Configure headers during newContext() and install routes before goto(). |
| Permission prompt still appears | The permission was granted for the wrong origin. | Pass the exact origin to grantPermissions() and grant it before navigation. |
| Tests pass alone but fail in parallel | Shared backend records, files or ports are colliding. | Use unique test data and resources; context isolation only covers browser-side state. |
| Artifacts are incomplete | The browser closed before the context flushed videos, HAR files or downloads. | Close contexts explicitly, then close the browser in a finally block. |
| Storage snapshot exposes credentials | The auth file was committed or uploaded. | Add it to ignore rules, restrict permissions and rotate credentials if exposed. |
Performance, reliability and cost
- Performance: contexts are cheaper than launching a browser for every test because the browser process can be shared. Reuse the browser, but create fresh contexts for isolation.
- Concurrency: limit the number of simultaneous contexts to the CPU, memory and application capacity available in your runner. More contexts do not make a slow target faster.
- Reliability: use explicit waits for application conditions, close contexts in
finally, and capture diagnostics on failure. A clean context improves reproducibility but cannot fix nondeterministic server data. - Network behavior: context-level routing and headers affect all pages in that context. Keep special-case routes in a dedicated context so they cannot alter another scenario.
- Cost: self-hosted Playwright or Puppeteer consumes your own compute and browser maintenance budget. For screenshot-only jobs, ScreenshotNeo bills only clean shots; bot checks, blank pages, timeouts, failed loads and cache hits are free.
Checklist for isolated sessions
- Create one context per independent test or user.
- Create pages from the correct context, never from a shared default by accident.
- Set headers, permissions, locale and routes before navigation.
- Use storage snapshots only for controlled sign-in reuse.
- Keep auth snapshots out of source control and build logs.
- Close every context before closing the browser.
- Separate browser isolation from server-side test-data isolation.
FAQ
Can a browser context run without launching a browser?
No. A context is created inside a browser instance. You can share one browser across many contexts to avoid repeated process startup.
Does a new page create a new login session?
No. Pages in the same context share that context’s cookies and storage. Create a new context for a new login.
Can I make a context survive a process restart?
Use a persistent context or save supported state with storageState(). Persistent profiles have different cleanup and security characteristics than non-persistent contexts.
Is context isolation the same as a separate machine?
No. Contexts isolate browser session data inside one browser process. They do not isolate CPU, memory, filesystem access or server-side application records.
When should I use an API instead of a browser context?
Use a browser context for interaction, authentication flows and multi-user behavior. Use a screenshot API such as ScreenshotNeo when you need rendered captures without maintaining browser setup.


