ScreenshotNeo

BlogGuides

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.

By the ScreenshotNeo team1 October 20268 min read

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.