ScreenshotNeo

BlogHow-to

How to Isolate Browser Sessions in Automated Tests

Use a fresh Playwright BrowserContext for each test to prevent browser state leaks, and isolate shared server-side data separately.

By the ScreenshotNeo team4 October 20267 min read

In Playwright, isolate each independent test in a fresh BrowserContext. A context has its own cookies, local storage, and session storage, while several contexts can share one browser process. Playwright Test already creates a context for each test when you use its standard page or context fixture.

A fresh context isolates browser-side session state. It does not isolate server-side accounts, database records, global settings, files, or external services. Use unique backend data or worker-specific accounts for those resources, and coordinate access when concurrent tests would modify the same shared state.

What a browser session includes

A browser context is an isolated browser session. Pages within one context share that context’s cookies and storage; pages in different contexts do not. This makes contexts a useful boundary for tests that need separate signed-in users or clean browser state. See Playwright’s BrowserContext isolation documentation.

Isolation has two boundaries to consider:

  • Browser state: cookies, local storage, session storage, and other context-scoped browser data. Use a fresh context for each independent test.
  • Application state: accounts, database rows, shared settings, external services, and files. Isolate these with unique records, separate test users, cleanup, or targeted serialization.

Use Playwright Test’s per-test fixtures

For ordinary tests, use the built-in page or context fixture. Playwright Test provides a new context for each test, so browser storage created by one test does not carry over to another.

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

test('a user can sign in', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByLabel('Email').fill('test@example.com');
  await page.getByLabel('Password').fill('test-password');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByText('Account')).toBeVisible();
});

test('starts without the previous test cookie', async ({ context }) => {
  const cookies = await context.cookies();
  expect(cookies).toEqual([]);
});

The example assumes the application’s login labels and test credentials are configured for your environment. Prefer a dedicated test account, and avoid using production credentials or data.

Create separate sessions for multiple users

When a scenario involves two identities—such as a buyer and an administrator—create two contexts from the same browser. Each gets independent cookies and storage, without launching another browser process.

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

test('users have separate sessions', async ({ browser }) => {
  const buyerContext = await browser.newContext();
  const adminContext = await browser.newContext();

  try {
    const buyerPage = await buyerContext.newPage();
    const adminPage = await adminContext.newPage();

    await buyerPage.goto('https://example.com');
    await adminPage.goto('https://example.com');

    // Sign in separately, or initialize each context with its own storage state.
    await expect(buyerPage).toHaveURL(/example\.com/);
    await expect(adminPage).toHaveURL(/example\.com/);
  } finally {
    await buyerContext.close();
    await adminContext.close();
  }
});

Close manually created contexts in a finally block, especially when setup or assertions can fail. The browser fixture itself is managed by Playwright Test.

Reuse authentication safely with storage state

Signing in through the UI for every test can add setup time. Playwright can save authenticated browser state and use it to initialize a fresh context for each test. This retains browser isolation while avoiding repeated login flows. The Playwright authentication guide covers setup projects and storage state.

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

setup('authenticate', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByLabel('Email').fill(process.env.TEST_EMAIL ?? '');
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD ?? '');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByText('Account')).toBeVisible();
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  use: {
    baseURL: 'https://example.com',
    storageState: 'playwright/.auth/user.json',
  },
});

Add the generated authentication directory to .gitignore. Storage state can contain cookies and other credentials that allow someone to act as the authenticated user. Keep it out of source control and restrict access to it.

A shared authenticated state is appropriate only if tests using it can run concurrently without changing server-side state in conflicting ways. If tests modify the same account, use separate accounts—for example, one per worker—or give each test unique application records.

Keep parallel tests independent

Playwright can run tests in parallel. Separate contexts prevent browser cookies and storage from leaking across tests, but parallel workers can still collide on the same backend data. See the official parallelism guidance.

  • Give each test a unique identifier for records it creates or edits.
  • Use a separate test account per worker when tests mutate account-level state.
  • Make setup and cleanup safe to retry, and ensure one test cannot delete another test’s data.
  • Serialize only the tests that share a resource and cannot safely run concurrently; use a single worker only when the shared resource requires it.

Fresh contexts are generally a better browser-state reset than trying to clear every cookie and storage key after each test. Cleanup can miss state that is difficult to enumerate or reset. A new context starts with a clean browser session; application data still needs its own lifecycle.

Python: create and close a fresh context

With Playwright’s Python library, create a non-persistent context for each independent session and close it when the test finishes. Non-persistent contexts do not write browsing data to disk. The API details are in the BrowserContext Python reference.

import asyncio
from playwright.async_api import async_playwright, expect

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        context = await browser.new_context()
        try:
            page = await context.new_page()
            await page.goto("https://example.com")
            await expect(page).to_have_title("Example Domain")
            print(await context.cookies())
        finally:
            await context.close()
            await browser.close()

asyncio.run(main())

For a test suite, put context creation and closing in the test framework’s fixture lifecycle so every test receives a new context, including when a test fails. Do not reuse one context across independent tests just to avoid setup.

Or skip the browser setup

If your task is to capture a page image rather than test interactive behavior, ScreenshotNeo takes a screenshot with one GET request. Its API accepts a URL and returns PNG, JPEG, WebP, or PDF; see the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000.

Sign up free for 1,000 screenshots a month, with no card required.

Troubleshooting session leaks and collisions

Cookies appear in a test that should be clean

Cause: The tests are reusing a context, or a custom fixture has a wider lifetime than the test. Fix: Use the built-in per-test fixture or create a new context for each test. Check fixture scope and close manually created contexts.

Two tests change the same user’s state

Cause: Context isolation does not create separate server-side accounts. Both tests can have different cookies while updating the same user record. Fix: Use distinct test users or isolate the records each test changes. Coordinate access if the resource cannot be separated.

A saved login works locally but fails in CI

Cause: The state file may not exist in the CI job, may be stale, or may have been generated for a different environment or account. Fix: Generate it as part of the test setup in the same environment, verify the setup login succeeds, and ensure the file is passed only to the intended tests.

Authentication state is missing or unexpectedly shared

Cause: The wrong storage-state path is configured, or several tests use the same authenticated account while mutating it. Fix: Confirm the path and setup dependency, then use worker-specific state or accounts when concurrent mutations are possible. Protect state files as secrets.

Tests pass alone but fail in parallel

Cause: Workers are racing on shared backend records, account settings, files, or external services. Fix: Assign unique data per test or worker and make cleanup ownership-specific. As a diagnostic or for an inseparable shared resource, serialize only the affected tests.

Performance, reliability, and cost

Creating contexts within one browser avoids the process startup cost of launching a separate browser for every test, while maintaining a separate browser session per test. Reusing authenticated storage can also avoid repeating UI login steps. These choices do not make shared backend data safe; concurrency and data ownership still determine whether parallel tests are reliable.

Fresh contexts reduce dependence on cleanup code for browser state. They cannot guarantee isolation of external state, and a test that uses a shared account can still be affected by another worker. Keep account and record allocation deterministic, make cleanup idempotent where practical, and avoid broad cleanup that can delete another test’s data.

FAQ

Can Playwright contexts share one browser?

Yes. Create multiple contexts from the same browser. Each context has separate cookies and storage while using the shared browser process.

Does a new context clear server-side sessions?

No. It starts with fresh browser-side state. A server-side session can still be affected if tests use the same account or backend records.

Should I clear cookies after every test instead?

A fresh context is the simpler boundary for independent tests because it starts with clean browser state. Clearing selected state is useful when a test specifically needs to verify cleanup behavior.

Can I use one saved login for every test?

Yes, when those tests do not make conflicting changes to the shared account. Use separate accounts or worker-specific state when tests mutate shared account data.