PRACTICAL GUIDE / Playwright browser context pooling risks

Stop pooling browser contexts in your Playwright suite

Learn how reused browser contexts leak authentication, routes, and permissions between Playwright tests, then migrate to clean per-test isolation.

By The Testing AcademyUpdated August 4, 202624 min read
All field guides
In this guide6 sections
  1. Why a clean page is not a clean session
  2. How pooled state creates three different failures
  3. How to prove the pool is responsible
  4. Replace the pool without giving up useful reuse
  5. Roll the change through an existing suite
  6. When context reuse is the wrong diagnosis

What you will learn

  • Why a clean page is not a clean session
  • How pooled state creates three different failures
  • How to prove the pool is responsible
  • Replace the pool without giving up useful reuse

The checkout test fails only after the admin test has run on the same worker. Run checkout alone and it passes. Run the failed test again through Playwright's retry mechanism and it passes there too. That pattern often points to a BrowserContext pool returning somebody else's session, not to a slow checkout page.

Pooling looks attractive when a suite spends time logging in or creating pages. The mistake is treating a BrowserContext like a database connection with a small, resettable state. It is closer to a temporary browser profile. Once a test can leave anything behind in that profile, the next borrower inherits a history it did not create.

Why a clean page is not a clean session

Playwright Test already performs the safe form of reuse. The built-in browser fixture is shared by tests in the same worker, which avoids launching a new browser process for every test. The built-in context fixture is created for one test, and the built-in page belongs to that context. This is the boundary described in Playwright's isolation guide and fixture documentation.

A home-grown pool usually changes the second half of that design. It keeps an array of BrowserContext objects, hands one to a test, then puts the same object back after closing the current page. The next test receives a new Page, so the screen looks clean. The session underneath it is unchanged.

That difference matters because a page is only one member of a context. Cookies are installed on the context and apply to its pages. Permission overrides are granted to the context. A handler registered with context.route() remains attached to that context until it is removed or the context closes. Context-level init scripts, exposed bindings, event listeners, and any other registrations attached to the same long-lived object have their own cleanup requirements. Popups also belong to the parent page's context, so closing the page you expected does not prove that no other page remains.

Browser storage makes the reset problem larger. A web application can touch local storage and session storage for more than one origin during sign-in, payment, embedded support, or an identity-provider redirect. A reset helper that evaluates localStorage.clear() on the final application page reaches one origin and one storage mechanism. It says nothing about a second origin visited earlier. It also says nothing about state that is not exposed through that call. Playwright's own isolation documentation warns that cleanup between tests is easy to get wrong and that some state, such as visited links, cannot be cleaned up reliably.

context.clearCookies() and context.clearPermissions() are real, useful APIs. They do exactly what their names say. They are not a general reset() operation, and Playwright does not document them as one. The presence of several cleanup methods is evidence that context state has several owners, not evidence that calling two methods restores a fresh profile.

Closing the BrowserContext has a much simpler contract. The BrowserContext API says that context.close() closes the context and all pages belonging to it. A later browser.newContext() creates another isolated, non-persistent context. That lifecycle turns cleanup from an expanding checklist into disposal.

Teams usually add a pool to save one of three costs: login time, repeated test setup, or perceived context creation time. Only the last one would be addressed directly by reusing a context, and Playwright deliberately makes contexts cheap enough to use as its test-isolation unit. Login time is better handled with authenticated storage state loaded into a new context. Repeated server setup is better handled through API fixtures or controlled test data. Neither optimization requires two unrelated tests to share a live browser session.

The ownership rule is straightforward. A worker may own a browser. A test owns its default context. If a test manually creates extra contexts for two users, that same test owns and closes them. A context pool inserts a fourth owner whose lifetime spans tests, failures, hooks, and retries. Most contamination bugs live in the gaps between those lifetimes.

How pooled state creates three different failures

Authentication leakage is the easiest case to recognize. An admin test signs in, the application sets a session cookie, and the pool releases the context. The release helper closes the page and perhaps clears a familiar cookie name. The next test requests an anonymous page but arrives as an administrator because another cookie, a local-storage token, or identity-provider state survived.

You do not need a live application to prove the cookie part of that mechanism. This characterization test uses a one-slot pool with the same lifecycle as the problematic implementation. It deliberately fails because release() returns a dirty context. If production code starts disposing returned contexts, or performs a cookie-only reset, the assertion changes behavior for a concrete reason.

TypeScript
import { expect, test, type Browser, type BrowserContext } from '@playwright/test';

class OneSlotContextPool {
  private idle: BrowserContext[] = [];

  async acquire(browser: Browser): Promise<BrowserContext> {
    return this.idle.pop() ?? browser.newContext();
  }

  async release(context: BrowserContext): Promise<void> {
    this.idle.push(context);
  }
}

test('a returned context must not contain the previous session cookie', async ({ browser }) => {
  const pool = new OneSlotContextPool();
  const producer = await pool.acquire(browser);

  await producer.addCookies([{
    name: 'session',
    value: 'admin-token',
    url: 'https://qa.example.test',
  }]);
  await pool.release(producer);

  const consumer = await pool.acquire(browser);
  try {
    const leaked = (await consumer.cookies('https://qa.example.test'))
      .find(cookie => cookie.name === 'session');

    expect(leaked, 'the pool returned authenticated browser state').toBeUndefined();
  } finally {
    await consumer.close();
  }
});

For this probe, the useful failure is the received cookie object at the assertion. Its name is session and its domain belongs to the probe origin. That evidence is available before navigation, so a redirect bug or stale DOM cannot explain it. Be careful with real diagnostics: assertion output can include cookie values. Attach names and domains when investigating CI, but do not publish session values in a report.

A second failure has no authentication symptom. A catalog test registers a context-level route to fulfill the product endpoint with a fixture. It closes its page and returns the context. Later, an integration test navigates to the same URL expecting the real service. The old route still handles the request, so the integration test passes against fixture data or fails because the fixture no longer matches the UI.

The self-contained probe below uses a response header to identify the owner of the response. There is no DNS dependency because the route fulfills the navigation. Closing the producer page demonstrates the exact near-miss: page cleanup succeeds, but the context route remains.

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

test('a route from the previous borrower still owns the next request', async ({ browser }) => {
  const context = await browser.newContext();
  const target = 'https://qa.example.test/catalog';

  try {
    await context.route(target, async route => {
      await route.fulfill({
        status: 200,
        contentType: 'text/html',
        headers: { 'x-test-owner': 'catalog-fixture' },
        body: '<h1>Fixture catalog</h1>',
      });
    });

    const producerPage = await context.newPage();
    await producerPage.close();

    const consumerPage = await context.newPage();
    const response = await consumerPage.goto(target);

    expect(response?.headers()['x-test-owner']).toBeUndefined();
  } finally {
    await context.close();
  }
});

That assertion fails with catalog-fixture as the received header value. The failure proves that a context-scoped route, not the network service, produced the response. Calling context.unrouteAll({ behavior: 'wait' }) during pool release would address this one registration class. It would not clear authentication, permissions, browser storage, pages, init scripts, bindings, or listeners. Adding it to a growing reset helper reduces one risk while making the pool look safer than it is.

The same ownership issue applies to context-level event listeners. Suppose a test listens for every response and writes selected bodies to an array for contract checks. If the listener is attached to a pooled context and never removed, the next test can invoke code owned by the first test. At best, it wastes work. At worst, it writes into closed resources, retains data longer than intended, or throws while a different test is running. A stack trace then points at the old listener, while the reporter labels the current test as failed.

Permission leakage is a third shape. A geolocation test grants permission on the context and configures coordinates for an application origin. An anonymous test later expects the product's location-denied fallback, but the browser call succeeds under the retained override. The product can be correct and the test can still time out waiting for UI that the pooled profile has bypassed. clearPermissions() can remove permission overrides, but a reliable reset would have to call it on every release path, including a failing test, a timeout, and a worker shutdown.

Origin storage produces similar evidence with a different fix. If the second test opens already with a dismissed onboarding flag, inspect local storage for that origin before blaming the feature flag service. If an identity-provider origin was part of the previous flow, checking only the application origin is incomplete. A test can also open a popup and leave it running. context.pages() gives you the pages currently known to the context; more than the expected blank or new page at acquisition is direct evidence that the pool accepted an occupied session.

These examples should not be collapsed into “clear browser data.” They have different owners and different observations. A cookie appears through context.cookies() before navigation. A stale route identifies itself in the response or trace network entry. A permission leak changes whether the browser prompts. A leftover popup appears in context.pages(). Recording the specific signal prevents a reset change from appearing to fix a failure it never exercised.

The worst outcome is a false pass. An authorization test may borrow the administrator's cookie and see a privileged control, then declare that the application correctly granted access to the user created by the test. A network test may borrow a successful mock and never contact the service it claims to cover. Both reports are green. Neither assertion tested the intended boundary.

How to prove the pool is responsible

Start at acquisition, before page.goto(). A dirty context is easiest to identify before the application gets a chance to modify it. Record cookie names and domains, the count and sanitized locations of open pages, the worker index, the retry index, and the test title. Do not record cookie values, authorization headers, full query strings, or storage-state files in general CI logs.

This helper produces a small JSON attachment that avoids cookie values and URL queries. Call it immediately after your legacy pool returns a context. The file is attached to the current test result, so it stays with the reporter output and trace rather than being mixed into one worker-wide log.

TypeScript
import type { BrowserContext, TestInfo } from '@playwright/test';

function locationWithoutQuery(rawUrl: string): string {
  try {
    const url = new URL(rawUrl);
    return url.origin + url.pathname;
  } catch {
    return rawUrl;
  }
}

export async function attachContextInventory(
  context: BrowserContext,
  testInfo: TestInfo,
): Promise<void> {
  const cookies = await context.cookies();
  const inventory = {
    test: testInfo.titlePath,
    workerIndex: testInfo.workerIndex,
    retry: testInfo.retry,
    pages: context.pages().map(page => locationWithoutQuery(page.url())),
    cookies: cookies.map(({ name, domain, path }) => ({ name, domain, path })),
  };

  await testInfo.attach('context-at-acquisition.json', {
    body: Buffer.from(JSON.stringify(inventory, null, 2)),
    contentType: 'application/json',
  });
}

A context inventory is diagnostic evidence, not an assertion that every test must start with zero cookies. Projects that load an approved authentication state will correctly begin with cookies. Compare the attachment with the project's declared baseline. An unexpected role cookie, an unapproved domain, or an existing page is the signal. If every context has the same intended baseline, the inventory should say so explicitly.

Then force the suspected order. Put one producer and one consumer in a narrow probe file. The producer installs the state you think is leaking. The consumer asserts the clean baseline before it navigates. Keep retries off because a retry changes the worker lifecycle. Use one worker so both tests cannot be separated across worker processes.

Shell
pnpm exec playwright test e2e/context-pool.probe.spec.ts --workers=1 --retries=0 --repeat-each=10 --reporter=line

Ten repeats are a stress setting in this command, not a claimed measurement or a recommended universal threshold. A deterministic two-test probe should fail on its first contaminated handoff. Repetition is useful when pool selection depends on the number of available contexts or on asynchronous release timing. Increase or decrease it based on the pool's actual capacity.

Next, reverse the producer and consumer. If the consumer passes before the producer and fails after it, the order is evidence. Run the consumer alone by line number. If it passes alone, its own setup is probably not creating the dirty state. Run the same probe with the pool disabled but the same application and test data. If that passes, the lifecycle change has isolated the variable.

Retries can make this bug look like ordinary flakiness. Playwright's retry documentation states that after a test failure the runner discards the worker process and its browser, then starts a new worker. A JavaScript pool stored in worker memory disappears with that process. The retry receives an empty pool and often passes. The report then labels the test flaky, which is consistent with contamination but does not prove it.

This detail also explains why a trace captured only on the first retry may be clean. The retry starts in the replacement worker. For an isolation investigation, trace: 'retain-on-failure' keeps a trace from the failed first attempt. The trace viewer can show the first request, DOM snapshots, console messages, and network activity from that attempt. Look for an already-authenticated first navigation, a response supplied without the expected server behavior, or UI state present before the test creates it.

A trace has a boundary too. It can show what happened during the failing test, but it may not show which earlier test dirtied a shared context. That is why the acquisition attachment and forced producer-consumer pair matter. The trace explains the consumer's actions. The inventory explains its starting condition. The ordered probe connects that condition to a producer.

Use the error shape to rule out lookalikes. A denial-fallback timeout that occurs only after a test grants location permission is consistent with context contamination. The same timeout in a fresh context with no permission override rules out the pool and points toward the application or assertion. A wrong account cookie before navigation is client state. A clean cookie inventory followed by an account that already owns the order points toward shared server-side data. A fixture response header points toward route leakage. A real response with stale data points elsewhere.

Do not diagnose from “passes locally, fails in CI” alone. CI may use another base URL, browser version, worker count, test order, clock, or account. Context pooling becomes the leading cause when the failure follows a prior borrower, disappears with fresh contexts, and leaves context-scoped evidence.

Replace the pool without giving up useful reuse

The smallest safe fix in Playwright Test is usually deletion. Remove the pool fixture and accept the built-in page or context fixture in each test. The browser process remains shared within its worker, so this does not turn every test into a full browser launch.

This pair is a useful regression check during migration. The first test installs a marker cookie. The second test proves that its built-in context does not inherit that marker. There is no cleanup hook because Playwright owns the fixture lifecycle.

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

const origin = 'https://qa.example.test';
const markerName = 'context-pool-regression-marker';

test('one test can install its own marker', async ({ context }) => {
  await context.addCookies([{
    name: markerName,
    value: 'producer',
    url: origin,
  }]);

  await expect.poll(async () => {
    return (await context.cookies(origin)).some(cookie => cookie.name === markerName);
  }).toBe(true);
});

test('the next test receives a fresh context', async ({ context }) => {
  const marker = (await context.cookies(origin))
    .find(cookie => cookie.name === markerName);

  expect(marker).toBeUndefined();
});

If the suite pooled contexts to avoid logging in through the UI, move that optimization to authenticated storage state. The authentication guide documents creating state once and loading it into fresh test contexts. Each test can start authenticated without receiving the live context used by another test.

Authentication state is sensitive. Store it under a directory excluded from version control, refresh it when accounts expire, and choose an account strategy that matches the application's server-side behavior. Loading the same state into separate contexts isolates client-side cookies and storage after creation. It does not stop two tests from changing the same server-side cart, profile, inbox, or subscription. For tests that mutate shared account data, allocate accounts per worker or per test, or clean server data through an owned API.

A role-specific fixture can hide the context creation details without pooling the context itself. The fixture below creates a new context initialized from one approved state file, gives the test a page, and closes the context during fixture teardown. The finally block keeps ownership visible even when the test or setup throws.

TypeScript
import {
  test as base,
  type Page,
} from '@playwright/test';

type RoleFixtures = {
  adminPage: Page;
};

export const test = base.extend<RoleFixtures>({
  adminPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'playwright/.auth/admin.json',
    });

    try {
      const page = await context.newPage();
      await use(page);
    } finally {
      await context.close();
    }
  },
});

export { expect } from '@playwright/test';

The state file must already exist, normally from a setup project. This fixture is not a complete authentication workflow on its own. Its important property is the scope: every use creates a context, and the same fixture closes that context after the test.

Multi-user tests are another legitimate reason to call browser.newContext() manually. Keep all users inside one test and close every context that test creates. Do not return either context to a cross-test pool.

Write that test carefully, because the obvious version of it proves nothing. Calling browser.newContext() twice inside one test and then asserting that the second context lacks a cookie written into the first is true by construction. Two independently created contexts are isolated the moment they exist, so no change to the application, the fixtures, or the pool could make that assertion fail. A title such as "admin state does not appear in the shopper session" then promises an application claim that the assertion never touches. That is the same defect this article warns about elsewhere, dressed up as a recommended pattern.

The version below is worth running because it routes both users through the same factory function, which is precisely where a pool gets reintroduced. If openUserContext() is later changed to hand back a cached or borrowed context, both roles land in one context, the shopper's cookie overwrites the admin's, and the admin assertion fails with Received: "shopper". The identity check backs that up in case a future variant returns the same object without a visible cookie collision. Both users are asserted positively, so the test states what each context should contain rather than only what one of them should lack.

TypeScript
import { expect, test, type Browser, type BrowserContext } from '@playwright/test';

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

async function openUserContext(
  browser: Browser,
  role: string,
): Promise<BrowserContext> {
  const context = await browser.newContext();
  await context.addCookies([{ name: 'role', value: role, url: origin }]);
  return context;
}

test('each user in one test gets a context the other cannot read', async ({
  browser,
}) => {
  const adminContext = await openUserContext(browser, 'admin');
  const shopperContext = await openUserContext(browser, 'shopper');

  const roleIn = async (context: BrowserContext) =>
    (await context.cookies(origin)).find(cookie => cookie.name === 'role')?.value;

  try {
    expect(await roleIn(adminContext)).toBe('admin');
    expect(await roleIn(shopperContext)).toBe('shopper');
    expect(adminContext).not.toBe(shopperContext);
  } finally {
    await Promise.all([
      adminContext.close(),
      shopperContext.close(),
    ]);
  }
});

The title now describes a harness property, which is what these assertions actually measure. If you want the application claim as well, add it as a separate case that signs both roles in and checks that an admin-only control is absent from the shopper's page. Cookie separation is browser evidence; a missing privileged control is product evidence. Keep the two claims in two tests so a failure names which one broke.

When a test needs a returning-user journey, retain the state inside that one test. Navigate away and back, reload, close and open another page in the same context, or exercise the exact persistence boundary the product promises. That is product behavior, not infrastructure reuse. The context still closes when the scenario finishes.

The trade-off is real. A new context has a creation cost, and authenticated state or server fixtures add maintenance. A suite may expose extra setup time after removing its pool. Measure the changed suite before choosing the next optimization. Reduce expensive UI login with storage state. Seed data through a supported backend API. Reuse immutable downloaded fixtures on disk. If CI is short on memory, control Playwright worker count. Sharing a mutable browser profile is a poor substitute for resource planning because it exchanges visible runtime cost for hidden order dependence.

Roll the change through an existing suite

Do not replace a mature pool in one blind edit. First find every acquisition and release path. Search for browser.newContext() in worker-scoped fixtures, beforeAll hooks, module-level arrays, generic resource-pool classes, and helpers that return a Page without exposing its owning context. Manual context creation inside one test is not automatically wrong, so review lifetime rather than banning an API call.

Document the current contract before changing it. Which context options does the pool apply? Does it load storage state, set locale, grant permissions, install routes, or create a starting page? Which code closes a page, and which code closes the context? What happens if fixture setup throws before release? What happens if a test times out? That inventory becomes the migration checklist.

Add the smallest producer-consumer probes for the state classes your suite uses. An authentication-heavy suite needs a cookie or origin-state probe. A service-virtualization suite needs a stale-route probe. A permissions suite needs a prompt or permission-baseline probe. Keep these tests next to the pool adapter while migrating, then retain at least one isolation regression after the pool is gone.

Change consumers in slices. Start with tests that already use the default page fixture and gain little from the pool. Next move read-only authenticated tests to fresh contexts initialized from storage state. Then move multi-user tests to contexts owned inside one test. Leave genuinely dependent, serial workflows until their shared assumptions have been made explicit.

During the rollout, run the probe in the most revealing topology: one worker, fixed order, and no retries. Also run the normal suite topology because teardown bugs can appear only with concurrency. Put the narrow check after the repository's existing dependency installation and Playwright browser setup steps. This YAML is a job-step fragment, not a complete workflow.

YAML
- name: Check browser context isolation
  run: >
    pnpm exec playwright test
    e2e/context-pool.probe.spec.ts
    --workers=1
    --retries=0
    --repeat-each=10
    --reporter=line

- name: Run the regular Playwright suite
  run: pnpm exec playwright test

Keep retries disabled for the isolation gate even if the regular suite uses them. A retry creates a fresh worker and can remove the state you are trying to observe. In the regular suite, preserve the first failed attempt with an appropriate trace mode and review flaky classifications instead of counting them as equivalent to first-run passes.

Remove reset code only after its consumers have moved. A large cleanup helper often contains application-specific work, such as revoking a server session or deleting a test cart, mixed with browser cleanup. Server cleanup may still be required with fresh contexts. Split it into an API or data fixture and keep its ownership explicit. Delete only the client reset that fresh contexts replace.

Watch capacity while the migration lands. If the pool previously capped the number of live contexts below the worker count, fresh per-test contexts may reveal that CI was relying on that accidental throttle. Set workers deliberately for the available machine instead of reintroducing shared sessions. Context concurrency, browser process concurrency, and test-account concurrency are separate limits and deserve separate controls.

A rollout is complete when a test cannot receive another test's context, manual contexts close in their owning fixture or test, and the isolation probe passes without retries. It is not complete merely because the old flaky test stopped appearing. The old order may have changed while the unsafe lifecycle remains.

When context reuse is the wrong diagnosis

Fresh contexts do not isolate the application database. Two tests can load perfectly clean browser profiles and still mutate the same customer record. The strongest clue is a clean context inventory followed by dirty state returned from the server. Check the test account, tenant, order identifier, and seed ownership. If both tests use the same backend identity, fix data allocation or cleanup rather than adding more browser resets.

A shared storage-state file can create this near-miss. Each test gets a separate client session initialized from the file, so there is no cookie leaking from one live context to another. Both sessions may still authenticate as the same user. One test changes that user's language or empties the cart, and the second observes the server-side change. New contexts are working correctly. The account strategy is not.

A stale route can also be local to one correctly isolated test. A helper may install context.route() at the start of the test and match a broader URL than intended. The trace shows a fulfilled request, but an acquisition inventory is clean and no earlier test is required. Narrow the route pattern or remove it inside that test. Context pooling is responsible only when the registration crosses the intended test boundary.

Likewise, a missing permission prompt is not proof of a leaked override. The application may remember consent on its server, the browser engine may handle that permission differently, or the test may attach its dialog logic after the event. Inspect the permission setup and reproduce with a new context before assigning blame. Playwright notes that supported permission names can vary across browsers and versions, so cross-browser differences deserve their own investigation.

Timeouts during context creation point in another direction. A saturated CI worker, browser crash, file-descriptor pressure, or slow environment can fail before a pool hands back any usable object. An isolation refactor might change the frequency by creating contexts more often, but it does not make every creation failure a contamination bug. Correlate browser process logs, worker exits, and resource telemetry. Then lower concurrency or repair the environment based on evidence.

There are also cases where retained context state is intentional. A test for “remember this device,” a shopping cart that survives a page restart, or a second tab that receives a live update needs continuity. Keep that continuity inside one scenario. The test should name the persistence boundary, perform the action that stores state, and verify behavior after the exact transition the user makes. Returning the context to an unrelated test would broaden the boundary beyond the product requirement.

A deliberately dependent workflow may use a shared page across several serial tests, and Playwright documents that pattern while recommending isolated tests in most cases. Treat it as one long scenario split for reporting, with the costs stated plainly: later steps cannot run independently, an early failure blocks later evidence, retries repeat the group, and order becomes part of the contract. A context pool for arbitrary tests has none of that clarity.

If you use Playwright as a library rather than Playwright Test, there is no runner fixture to close contexts for you. The answer is still ownership, not pooling by default. Create a context for one job or scenario, perform the work, and close it in finally. Reuse the launched Browser when that matches your process model. If throughput requirements eventually justify a specialized pool, its reset contract needs browser-engine-specific validation for every state category the jobs can touch. That is substantially more engineering than an array of idle contexts.

Do not add a pool to make a test pass by preserving setup from a previous test. That converts a missing fixture into a hidden dependency. Put required setup in the test's fixtures, load a named authentication baseline, or combine the steps into one scenario. A test that cannot explain where its initial state came from cannot give trustworthy evidence when it passes.

The final discriminator is the starting condition. If a context is dirty before the failing test acts and the dirt follows a prior borrower, repair the context lifecycle. If the context matches its declared baseline but the server, browser process, or test setup is wrong, follow that owner instead.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 4, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Can I reuse the Playwright browser between tests?

Yes. The built-in `browser` fixture is shared by tests in a worker, while each test receives an isolated `context` and `page`. Reusing the browser process keeps the useful optimization without sharing a browser session.

Does clearing cookies make a pooled BrowserContext safe?

No. Cookies are only one kind of context state. Routes, permission overrides, open pages, origin storage, init scripts, and listeners can also outlive the test that installed them.

Why does a context leak often pass on retry?

A retry normally runs after Playwright discards the failed worker process and starts another one. A pool stored in that worker starts empty, so the retry can pass even though the first attempt exposed a real order dependency.

How do I test two users without pooling contexts?

Create both contexts inside the same test with `browser.newContext()` and close them in a `finally` block. The two sessions can interact with one product flow without becoming shared infrastructure for later tests.

When is browser context reuse acceptable?

Only when retained state is the behavior under test, such as a returning-user journey inside one scenario. Keep that lifecycle inside a single test or an explicitly dependent workflow, and do not lend the context to unrelated tests.