Skip to main content
Back to Blog
Guide
2026-04-01

Playwright BrowserContext Guide for Isolation and Parallel Sessions

Use Playwright BrowserContext for clean test isolation, independent multi-user sessions, reusable auth state, context-wide controls, and safe parallel execution.

One Playwright browser containing isolated admin and member BrowserContext sessions

A Playwright BrowserContext is an isolated, non-persistent browser session. Use the built-in context and page fixtures for ordinary tests; Playwright creates a fresh context per test, isolating cookies, local storage, session storage, and other browser profile state. Create additional contexts from the browser fixture only when one scenario needs independent users or configurations. Contexts share a browser process but not session state, so they are the correct boundary for admin/member, buyer/seller, or sender/receiver flows. See the Playwright E2E complete guide for full project setup.

This guide answers the implementation questions that appear when a suite moves beyond one page: which object owns state, how to run two identities safely, when to reuse storageState, what parallel workers actually share, and why closing pages or clearing cookies is not equivalent to starting clean.

What BrowserContext Is and Is Not

The official isolation guide describes BrowserContexts as fast, isolated, incognito-like profiles. Playwright Test creates one context for each test and gives the test a default page inside it. With the library API, you create the same boundary manually through browser.newContext(), open pages, and close the context when finished.

Browser, BrowserContext, Page, and a test-runner worker solve different problems:

ObjectOwns or controlsIsolation implicationTypical lifecycle
BrowserBrowser engine process and contextsContexts in one browser are independent sessionsUsually one per worker
BrowserContextCookies, origin storage, permissions, emulation, routes, pagesSecurity/session boundary between usersFresh per Playwright Test test
PageOne tab, frames, navigation, page-scoped eventsPages in one context are not independent usersOne default page plus popups/tabs
Worker processTest files, worker fixtures, browser instanceSeparate OS process; cannot share in-memory variables with other workersReused until failure or run completion
Backend account/dataServer-side identity and recordsNot isolated by BrowserContextMust be partitioned by test or worker

The BrowserContext API says normal contexts created with browser.newContext() are non-persistent and do not write browsing data to disk. This does not mean they have no state while open. Pages in the context share cookies and local storage for the same origin, popups remain in the parent page's context, and context-level routes or permissions apply across its pages.

Use the Built-In Fixtures by Default

Most tests should not call browser.newContext() at all:

import { test, expect } from '@playwright/test';

test('shows the account dashboard', async ({ page, context }) => {
  // context is fresh for this test; page belongs to it.
  await page.goto('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  expect(page.context()).toBe(context);
});

The test runner handles creation and teardown. A second test receives a different context even if the worker process and browser are reused. That is the “start from scratch” strategy Playwright recommends over trying to clean every form of browser state between tests.

Do not put a BrowserContext in a module-level variable or a beforeAll merely to avoid login time. That changes unrelated tests into one ordered state machine. Save authenticated state once and initialize each fresh context from it instead. The official authentication guide shows setup-project and per-worker patterns for this purpose.

Create manual contexts when one test genuinely requires two simultaneous identities, different context options, or explicit library-style lifecycle control. Close every manual context in finally or a fixture teardown so a failed assertion does not leak pages, videos, routes, or downloads.

What State Crosses Pages and What Crosses Contexts

Use the ownership boundary to predict behavior:

State or capabilityShared by pages in one context?Shared by separate contexts?Persistence tool
CookiesYesNostorageState can save/restore them
localStorage for an originYesNoIncluded in storageState
sessionStorageNo, it is page-session scopedNoCapture manually; not in storageState
Permissions and geolocation overridesContext-levelNoContext options or methods, not storage state
Network routesContext routes cover its pagesNoRe-register in each context/fixture
Pages and popupsBelong to the same contextNoNot persisted as browser state
Virtual WebAuthn passkeysContext-scoped in 1.61NoSeed/install imperatively, not storage state
Server-side account recordsExternal to the browserPotentially yesProvision unique data/accounts yourself

The session-storage row is easy to misread. Two tabs in one signed-in context can share cookies and local storage yet have different session storage. Conversely, two contexts initialized from the same auth file have separate browser copies but can still mutate the same backend account.

For direct store manipulation in 1.61, see the localStorage and sessionStorage API guide. For passkeys, use the separate WebAuthn Credentials guide; neither state type changes the underlying context boundary.

Example 1: Run Two Independent Users in One Test

This complete TypeScript spec is runnable without an external server. Two contexts visit the same routed HTTPS origin. Each receives a different cookie and local-storage value, and each page renders only its own session. The same Browser instance owns both contexts.

// tests/multi-user-context.spec.ts
import { test, expect, type BrowserContext } from '@playwright/test';

const origin = 'https://sessions.example.test';
const html = `
  <h1>Session</h1>
  <output data-testid="cookie"></output>
  <output data-testid="workspace"></output>
  <script>
    document.querySelector('[data-testid=cookie]').textContent = document.cookie;
    document.querySelector('[data-testid=workspace]').textContent =
      localStorage.getItem('workspace') || 'none';
  </script>
`;

async function routeApp(context: BrowserContext) {
  await context.route(origin + '/**', route =>
    route.fulfill({ contentType: 'text/html', body: html }),
  );
}

test('keeps admin and member sessions isolated', async ({ browser }) => {
  const adminContext = await browser.newContext();
  const memberContext = await browser.newContext();

  try {
    await routeApp(adminContext);
    await routeApp(memberContext);

    await adminContext.addCookies([{ name: 'role', value: 'admin', url: origin }]);
    await memberContext.addCookies([{ name: 'role', value: 'member', url: origin }]);

    const adminPage = await adminContext.newPage();
    const memberPage = await memberContext.newPage();
    await Promise.all([adminPage.goto(origin), memberPage.goto(origin)]);

    await adminPage.localStorage.setItem('workspace', 'operations');
    await memberPage.localStorage.setItem('workspace', 'quality');
    await Promise.all([adminPage.reload(), memberPage.reload()]);

    await expect(adminPage.getByTestId('cookie')).toContainText('role=admin');
    await expect(adminPage.getByTestId('workspace')).toHaveText('operations');
    await expect(memberPage.getByTestId('cookie')).toContainText('role=member');
    await expect(memberPage.getByTestId('workspace')).toHaveText('quality');
  } finally {
    await adminContext.close();
    await memberContext.close();
  }
});

The two goto() and reload operations can proceed together because the sessions are independent. Promise.all coordinates known operations; it is not a substitute for test-runner parallelism and it does not make shared backend data safe.

In a real admin/member scenario, initialize each context with a role-specific storage-state file and assert identity before authorization behavior. Never log in as admin in one page and member in another page of the same context. The second login changes shared cookies and can silently turn both pages into the same user. The existing multiple-role auth-state guide shows a typed fixture for that pattern.

Example 2: Save State, Restore It, and Prove the Limits

The following offline-runnable spec captures a cookie and local-storage value from one context, initializes another context from the returned object, and proves that session storage was not restored.

// tests/context-storage-state.spec.ts
import { test, expect, type BrowserContext } from '@playwright/test';

const origin = 'https://state.example.test';

async function installPage(context: BrowserContext) {
  await context.route(origin + '/**', route =>
    route.fulfill({
      contentType: 'text/html',
      body: '<h1>State probe</h1>',
    }),
  );
}

test('restores cookie and local storage into a fresh context', async ({ browser }) => {
  const source = await browser.newContext();
  await installPage(source);
  await source.addCookies([{ name: 'session', value: 'test-user', url: origin }]);
  const sourcePage = await source.newPage();
  await sourcePage.goto(origin);
  await sourcePage.localStorage.setItem('theme', 'dark');
  await sourcePage.sessionStorage.setItem('draft', 'not-persisted');

  const state = await source.storageState();
  await source.close();

  const restored = await browser.newContext({ storageState: state });
  try {
    await installPage(restored);
    const page = await restored.newPage();
    await page.goto(origin);

    expect(await restored.cookies(origin)).toEqual(
      expect.arrayContaining([expect.objectContaining({ name: 'session', value: 'test-user' })]),
    );
    expect(await page.localStorage.getItem('theme')).toBe('dark');
    expect(await page.sessionStorage.getItem('draft')).toBeNull();
  } finally {
    await restored.close();
  }
});

The BrowserContext reference defines storage state as cookies, local storage, and an optional IndexedDB snapshot. The auth guide documents session storage separately. If Firebase or another login implementation stores tokens in IndexedDB, request it explicitly with storageState({ indexedDB: true }).

Treat a saved auth file as a credential. It can contain cookies and tokens that impersonate a test user. Keep it under playwright/.auth or the project's output directory, exclude it from version control, and avoid public report attachments. Delete or regenerate expired state rather than making every test tolerate anonymous redirects.

Context Options Belong at Context Creation

Context options are ideal for scenarios that need consistent browser-level conditions across all pages:

const context = await browser.newContext({
  locale: 'en-GB',
  timezoneId: 'Europe/London',
  colorScheme: 'dark',
  geolocation: { latitude: 51.5074, longitude: -0.1278 },
  permissions: ['geolocation'],
  serviceWorkers: 'block',
});

Set these through project use options when every test in a project needs them. Create an additional context only when one test compares configurations side by side. Contexts are cheap relative to separate browser launches, but unnecessary manual lifecycle still increases setup code and the chance of leaks.

Permission support can differ by browser and browser version; the API reference explicitly warns about that. Test the user-visible fallback as well as the granted path. Locale, timezone, and geolocation are independent controls: changing coordinates does not automatically change timezone or language.

Context routes apply to requests from all pages in the context, making them preferable for popups or multi-page flows. Playwright notes that route interception does not see requests already intercepted by a service worker and recommends blocking service workers when routing behavior is the goal. If service-worker behavior is itself under test, keep it enabled and use the dedicated service-worker APIs and limitations.

BrowserContext and Parallel Test Execution

Browser contexts provide test isolation; worker processes provide concurrency. The parallelism guide says test files run in parallel by default, tests within one file run in order by default, and each worker starts its own browser. Playwright reuses a worker where possible, but still creates a fresh context for every test using the standard fixtures.

Turning on fullyParallel allows individual tests in files to run across workers:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 4 : undefined,
});

Do this only after tests and test data are independent. Context isolation prevents browser-state leakage, but it cannot stop two workers from editing the same database row, draining the same queue, or revoking the same user's session. Use unique resources or worker-specific accounts. The official auth guide uses test.info().parallelIndex to select or create one account per parallel worker and saves each worker's state separately.

Do not create a global browser yourself inside test files to “improve parallelism.” The runner already manages browsers, contexts, retries, projects, traces, and teardown. Manual browser ownership bypasses those integrations and usually makes failures harder to attribute.

Choose the Smallest Context Pattern That Fits

Use one decision rule: keep the runner-owned context unless the scenario itself requires another independent browser session.

ScenarioContext pattern
Ordinary one-user behaviorBuilt-in page and context fixtures
Same tests under several roles or browsersProjects with role-specific storageState and device options
Two actors interacting in one scenarioTwo manual contexts or typed context fixtures created from browser
Many parallel tests need reusable unique accountsWorker fixture creates one auth file per parallelIndex; tests still get fresh contexts
Product explicitly depends on a disk profileDeliberate persistent-context suite, separated from normal isolated tests

This choice keeps reports and teardown aligned with Playwright Test. It also makes code review straightforward: every manual newContext() should correspond to a named actor or configuration that could not use the built-in context. If no such reason exists, remove it.

Context-Level Events, Routes, and Cleanup

Use context events when behavior can happen in any page. A context page event captures a popup regardless of which page opened it, and context request/response events observe traffic across pages. Page-level listeners are better when the scenario intentionally concerns one tab.

Close the context, not just its currently visible page, when the session is finished. context.close() closes every page in that context and finalizes context-owned artifacts. A useful reason can be supplied to interrupted operations in supported versions:

await context.close({ reason: 'multi-user scenario finished' });

Avoid closing the built-in context fixture manually; let the runner tear it down. Manual contexts created from browser are your responsibility. A fixture is often the cleanest owner because its code after await use(value) runs as teardown even when the test fails.

Failure Diagnosis by Ownership Boundary

Two pages unexpectedly become the same user

They are probably in one context. Pages share context cookies, so the most recent login changed both. Create one context per identity, initialize each with its own state, and assert an identity marker before testing collaboration or authorization.

A fresh context is still affected by another test

Determine whether the state is actually browser-side. Shared database records, cache entries, feature flags, email inboxes, and test accounts survive context creation. Partition or reset the external resource. If the leak is browser-side, look for a manually reused context, persistent profile, or state file shared unintentionally.

storageState loads but the app redirects to login

The cookie or token may have expired, the file may belong to another origin/environment, IndexedDB may have been omitted, or login completion may have been captured before redirects finished setting cookies. Regenerate state after an observable signed-in condition. Do not add retries around an invalid credential file.

sessionStorage is missing after state restoration

That is expected. Save selected session values separately and restore them before application scripts with an origin-restricted init script. The Web Storage guide includes a complete 1.61 capture/restore example.

Context routing misses a request

Check whether a service worker handled it, whether the URL pattern is correct, and whether the route was registered before navigation/request creation. Page routes take precedence when both match. For general network mocking, block service workers unless their behavior is the subject of the test.

target page, context, or browser has been closed

Find the lifecycle owner. A helper may close a context still used by another page, a finally block may run too early, or browser shutdown may interrupt contexts. Keep creation and closure in the same fixture/helper and avoid sharing manual contexts across concurrently running tests.

Parallel tests pass alone but fail together

Browser context isolation is working, but external data is colliding. Include a run ID and parallelIndex in accounts or resource names, make cleanup idempotent, and avoid tests that mutate shared account-level settings. Reducing workers can confirm the diagnosis but is not the final fix.

Version Scope and Limitations

BrowserContext isolation and the standard test fixtures long predate Playwright 1.61. Do not describe contexts, browser.newContext(), per-test isolation, parallel workers, or storageState as 1.61 additions. Relevant recent milestones in the current official docs are setStorageState() and isClosed() in 1.59, several context-wide page lifecycle events in 1.60, and browserContext.credentials in 1.61.

This guide targets non-persistent contexts and Playwright Test's normal runner lifecycle. Persistent profiles have different disk and single-profile constraints and should be used only when the product scenario genuinely requires a user data directory. BrowserContext isolation also does not emulate separate machines, IP addresses, browser processes, or backend tenants. Use projects, proxies, separate workers/machines, and isolated test infrastructure when those are the real boundaries.

Service-worker inspection is Chromium-specific in the current docs, and supported permissions vary. Storage-state files exclude session storage and passkeys; IndexedDB capture is optional. These are explicit limits, not reasons to abandon contexts.

For reliable actions inside each session, apply the Playwright locator best practices. The older browser-context isolation article offers additional fixture variations. Browse the QA skills directory or install the Playwright CLI skill for agent-assisted browser inspection, but keep context ownership, auth files, and parallel account allocation explicit in reviewed test code.

Frequently Asked Questions

Does Playwright create a new BrowserContext for every test?

Yes, when tests use the standard Playwright Test fixtures. Each test gets an isolated context and a default page. Manual library scripts must create and close contexts themselves.

Are two pages in one context isolated users?

No. They share cookies and origin-local storage, and a login in one can change the other's identity. Use separate contexts for separate users. Page-specific session storage does not turn pages into secure identity boundaries.

Is BrowserContext the same as an incognito window?

It is an incognito-like, isolated non-persistent profile. The analogy is useful for cookies and storage, but tests should rely on Playwright's documented API rather than browser UI assumptions.

Can multiple contexts run in parallel in one browser?

Yes. A single scenario can operate several independent contexts, and Playwright Test workers also run tests concurrently. Parallel safety still depends on isolating backend accounts and data, not just browser state.

Should I reuse one context to make tests faster?

Not across unrelated tests. The official isolation guidance favors a fresh context because cleanup is incomplete and easy to forget. Reuse authenticated storageState in fresh contexts instead of reusing the context itself.

What does BrowserContext storageState save?

It saves cookies and local storage, plus IndexedDB when explicitly requested. It does not persist session storage or the 1.61 virtual WebAuthn authenticator. Protect auth-state files as credentials.

When should I create a context from the browser fixture?

Create one when a single test needs another independent user, another set of context options, or explicit lifecycle control. Use the built-in context for ordinary one-user tests so the runner owns cleanup and artifacts.

Why do parallel tests still interfere when contexts are isolated?

They are usually sharing something outside the browser: an account, database record, feature flag, queue, mailbox, or environment. Allocate unique external state by test or worker and make cleanup deterministic.