Choosing Test Isolation in Playwright: Per-Test Contexts, Shared Contexts, or New Browsers
Playwright's default per-test BrowserContext is almost always the right isolation choice. This guide compares it against shared contexts and fresh browser launches, and shows the storageState setup-project pattern for fast, isolated authenticated tests.
15 Jan 2026, 19:53 UTC

The decision you're actually making
Every Playwright suite has to answer one question: how much isolation does each test get? Get it wrong in one direction and your suite is slow for no benefit. Get it wrong in the other and you inherit order-dependent failures that only appear in CI at 2 a.m. The useful takeaway: Playwright's default — a fresh BrowserContext per test — is almost always the right answer, and the two alternatives (sharing a context, launching a whole browser) should be deliberate, measured exceptions.
A BrowserContext is an isolated browser profile inside a single browser process: separate cookies, localStorage, cache, and permissions, but no new process. Creating one costs milliseconds, which is why Playwright Test builds its page and context fixtures on it.
Comparing the three supported options
| Approach | Isolation level | Cost per test | When it's justified |
|---|---|---|---|
| Per-test context (default fixtures) | Storage, cookies, cache | Milliseconds | Default for nearly all suites |
Manual browser.newContext() inside a test | Same, multiple at once | Milliseconds each | Multi-user flows: chat, approvals, admin + customer in one test |
| Worker-scoped shared context | None between tests in a worker | Amortized setup | Expensive, unavoidable per-test setup — with flake risk |
browserType.launch() per test | Full process | Seconds | Crash/recovery or process-level isolation testing |
Why the default wins
Per-test contexts compose cleanly with Playwright's parallelism model. Parallelism is controlled by workers (separate processes) and the fullyParallel config flag. Because each test owns its context regardless of which worker runs it, you can shard across CI machines or crank --workers=4 without changing test code. Shared contexts break this property: tests become coupled to their worker siblings.
The common reason people reach for a shared context is a slow login flow repeated in every test. The supported answer is not sharing a live context — it's storageState. You log in once in a setup project, save the resulting cookies and localStorage to a file, and every test's fresh context starts pre-authenticated. Isolation stays intact; the login cost is paid once per run.
Concrete implementation: setup project plus storageState
This pattern uses a setup project with a dependency, a well-established Playwright Test feature. Verify option names against your pinned Playwright version, since config details shift between releases.
// playwright.config.ts\nimport { defineConfig } from '@playwright/test';\n\nexport default defineConfig({\n fullyParallel: true,\n workers: process.env.CI ? 4 : undefined,\n projects: [\n { name: 'setup', testMatch: /auth\\.setup\\.ts/ },\n {\n name: 'chromium',\n use: { storageState: 'playwright/.auth/user.json' },\n dependencies: ['setup'],\n },\n ],\n});// tests/auth.setup.ts\nimport { test as setup, expect } from '@playwright/test';\n\nconst authFile = 'playwright/.auth/user.json';\n\nsetup('authenticate', async ({ page }) => {\n await page.goto('/login');\n await page.getByLabel('Email').fill(process.env.E2E_USER!);\n await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);\n await page.getByRole('button', { name: 'Sign in' }).click();\n // Wait for a post-login signal before saving state.\n await expect(page.getByRole('link', { name: 'Dashboard' })).toBeVisible();\n await page.context().storageState({ path: authFile });\n});Run with npx playwright test from your project root; no special permissions beyond your normal dev environment. The setup project runs first, writes the auth file, and dependent projects reuse it. Add playwright/.auth/ to .gitignore — it contains live session cookies.
When you genuinely need more than one context
Multi-party scenarios — a support agent and a customer chatting, an approver acting on a submitter's request — need two independent contexts inside one test. Create them manually from the browser fixture:
test('agent sees customer message', async ({ browser }) => {\n const customerCtx = await browser.newContext({ storageState: customerAuth });\n const agentCtx = await browser.newContext({ storageState: agentAuth });\n const customer = await customerCtx.newPage();\n const agent = await agentCtx.newPage();\n // ...drive both sides, assert cross-visibility...\n await customerCtx.close();\n await agentCtx.close();\n});Always close manually created contexts; leaked contexts accumulate memory in the worker and can slow later tests in the same process.
The shared-context trap
A worker-scoped fixture that reuses one context across tests can shave real time off suites with heavy per-test setup. The costs: leftover storage, registered service workers, and in-flight requests carry between tests, so a failure may depend on execution order and become impossible to reproduce locally with a single-test run. Shared contexts can also mask real application bugs — missing cleanup logic that your users will hit. If you accept this trade-off, quantify it first: time the suite both ways and confirm the speedup is worth the debugging burden.
Limitations and how to verify
storageState captures cookies and localStorage only — not sessionStorage, IndexedDB in all cases, or in-memory state. Apps that gate auth on those still need per-test login. Also note the saved state expires like any session; long CI queues can invalidate it, in which case the setup project should run per shard.
Practical checks:
- Run
npx playwright test --workers=4 --fully-parallelrepeatedly (or--repeat-each=5). Order-dependent failures signal leaked shared state. - Inspect the saved
user.json: confirm it contains the expected cookies and origin entries before wiring it intodependencies. - Compare wall-clock time of per-test contexts versus any shared-context experiment before accepting the isolation risk.
Start with the default. Add storageState when login dominates. Reserve shared contexts and fresh browsers for cases you can justify with a measurement.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.