PRACTICAL GUIDE / playwright worker scoped account fixture

Build a Worker-Scoped Account Pool for Parallel Playwright Tests

Build a worker-scoped Playwright account pool with atomic leases, stable parallel identity, isolated browser contexts, and resilient cleanup.

By The Testing AcademyUpdated July 11, 202610 min read
All field guides
In this guide9 sections
  1. Define the Concurrency Contract
  2. Create a Stable Lease Owner
  3. Lease Accounts Atomically
  4. Authenticate Once, Isolate Every Test Context
  5. Isolate Server Data Within the Worker
  6. Survive Failure, Retry, and Process Loss
  7. Diagnose Pool Failures With Lease Evidence
  8. Operational Checklist for Account Pools
  9. Rollout Plan

What you will learn

  • Define the Concurrency Contract
  • Create a Stable Lease Owner
  • Lease Accounts Atomically
  • Authenticate Once, Isolate Every Test Context

Parallel tests need parallel identities whenever they mutate account-level state. Sharing one login across workers looks efficient until one test changes a preference, another deletes a saved address, and a third asserts a notification count. The resulting failures are order-dependent because the tests are competing through the product, not because Playwright scheduling is unreliable.

A worker-scoped account pool gives each worker an exclusive account lease while avoiding per-test account provisioning. That optimization is safe only when the lease is atomic, its owner identity survives worker replacement, each test gets fresh browser state, and cleanup does not depend solely on a graceful process exit.

Define the Concurrency Contract

Write the account isolation rule before implementing fixtures. One leased account may be used by one worker at a time. Tests within that worker run sequentially, but they can still leak server state to later tests, so mutable records need per-test cleanup, reset, or unique names. Different browser projects or CI machines must not accidentally claim the same account.

The Playwright parallelism documentation exposes both workerIndex and parallelIndex. A restarted worker receives a new worker index but retains its parallel index. That makes parallelIndex a better slot identity, but it is not globally unique. Combine it with a run ID, shard ID, and project name when leases are shared outside one runner process.

Animated field map

Worker-Scoped Account Lease Lifecycle

A stable worker owner leases one account, creates isolated authenticated contexts, then releases with crash recovery available.

  1. 01 / worker identity

    Worker identity

    Combine run, shard, project, and parallel index into one owner.

  2. 02 / account lease

    Account lease

    Atomically reserve one eligible account for that owner.

  3. 03 / authenticated context

    Authenticated context

    Bootstrap fresh per-test browser state from worker authentication.

  4. 04 / isolated test batch

    Isolated test batch

    Namespace records and reset shared server state between tests.

  5. 05 / account cleanup

    Account cleanup

    Release idempotently, with expiration for interrupted workers.

Account roles belong in the pool contract too. A buyer test should never receive an admin account merely because the buyer pool is exhausted. Fail setup with capacity information rather than weakening role constraints.

Create a Stable Lease Owner

Do not use email addresses derived only from workerIndex. After a test failure, Playwright may replace the worker and assign a different index, leaving the old account orphaned and consuming another one. A stable owner key lets an idempotent lease endpoint return the same active lease to the replacement worker.

TypeScript
import type { WorkerInfo } from '@playwright/test'

function leaseOwner(workerInfo: WorkerInfo): string {
  const runId = process.env.QA_RUN_ID
  const shard = process.env.QA_SHARD_ID ?? 'local'
  if (!runId) throw new Error('QA_RUN_ID is required for account leasing')

  return [
    runId,
    shard,
    workerInfo.project.name,
    `slot-${workerInfo.parallelIndex}`,
  ].join(':')
}

Generate QA_RUN_ID once per pipeline, not independently in every worker. When local development does not use a remote shared pool, a launcher can provide a unique run ID for that invocation. Keep the owner short enough for logs and include it in failure attachments.

Lease Accounts Atomically

A static array indexed by parallel slot works only when one isolated runner owns the array. A central pool used by multiple jobs needs an atomic lease operation in a test-support service or database. The endpoint should select an available account with the required role, bind it to the owner and expiry, and return an existing lease when the same owner retries.

TypeScript
// fixtures/accounts.ts
import {
  test as base,
  type APIRequestContext,
} from '@playwright/test'

type AccountLease = {
  leaseId: string
  accountId: string
  email: string
  password: string
}

type WorkerFixtures = {
  leaseApi: APIRequestContext
  accountLease: AccountLease
}

export const accountTest = base.extend<{}, WorkerFixtures>({
  leaseApi: [async ({ playwright }, use, workerInfo) => {
    const baseURL = workerInfo.project.use.baseURL
    const poolToken = process.env.QA_POOL_TOKEN
    if (typeof baseURL !== 'string') throw new Error('Project baseURL is required')
    if (!poolToken) throw new Error('QA_POOL_TOKEN is required')

    const api = await playwright.request.newContext({
      baseURL,
      extraHTTPHeaders: {
        authorization: `Bearer ${poolToken}`,
      },
    })
    await use(api)
    await api.dispose()
  }, { scope: 'worker' }],

  accountLease: [async ({ leaseApi }, use, workerInfo) => {
    const owner = leaseOwner(workerInfo)
    const response = await leaseApi.post('/api/test-support/account-leases', {
      data: { owner, role: 'buyer', ttlSeconds: 900 },
    })
    if (!response.ok()) {
      throw new Error(`Account lease failed for ${owner}: ${response.status()}`)
    }

    const lease = (await response.json()) as AccountLease
    await use(lease)

    const release = await leaseApi.delete(
      `/api/test-support/account-leases/${lease.leaseId}`,
    )
    if (![200, 204, 404].includes(release.status())) {
      throw new Error(`Account release failed: ${release.status()}`)
    }
  }, { scope: 'worker', timeout: 30_000 }],
})

The 404 release is treated as success because the lease may already have expired or been swept. Define this idempotency explicitly in the service contract. Do not log passwords or pool tokens when setup fails.

Authenticate Once, Isolate Every Test Context

Worker-scoped authentication can produce a storage-state object once, while each test creates a fresh browser context from it. This separates cookies, local storage mutations, permissions, and pages between tests even though the server account remains shared.

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

type TestFixtures = { memberPage: Page }
type StoredSession = Awaited<ReturnType<BrowserContext['storageState']>>
type WorkerFixtures = {
  accountLease: AccountLease
  workerStorageState: StoredSession
}

export const test = base.extend<TestFixtures, WorkerFixtures>({
  workerStorageState: [async ({ browser, accountLease }, use) => {
    const context = await browser.newContext()
    const page = await context.newPage()
    await page.goto('/sign-in')
    await page.getByLabel('Email').fill(accountLease.email)
    await page.getByLabel('Password').fill(accountLease.password)
    await page.getByRole('button', { name: 'Sign in' }).click()
    await expect(page.getByTestId('account-id')).toHaveText(accountLease.accountId)

    await use(await context.storageState())
    await context.close()
  }, { scope: 'worker' }],

  memberPage: async ({ browser, workerStorageState }, use) => {
    const context = await browser.newContext({ storageState: workerStorageState })
    const page = await context.newPage()
    await use(page)
    await context.close()
  },
})

In a real framework, merge this fixture definition with the account lease module so runtime dependencies and types align. Keeping state in memory avoids shared auth files and file-name collisions. The authentication guide warns that stored browser state can contain sensitive cookies and headers, so never commit generated state.

Isolate Server Data Within the Worker

Fresh contexts do not reset the server. A test that changes the leased account's locale can affect the next test even though cookies are new. Choose one of three strategies based on product behavior: reset mutable account state before each test, create child resources with a unique test key and delete them after use, or lease an account per test for suites whose scenarios fundamentally conflict.

Use testInfo.testId or a sanitized combination of run and test identity as a resource prefix. Avoid depending on test title alone because titles can repeat across projects. Cleanup endpoints should delete only records owned by the current test namespace.

Worker scope is an optimization, not a universal rule. Security scenarios that rotate credentials, tests that delete the current user, and account-level concurrency tests need dedicated accounts. Make an opt-out fixture or separate project rather than adding restoration logic so complicated that account state is no longer trustworthy.

Capacity follows the maximum workers that can hold each role concurrently across every shard and project using the same pool. Add a deliberate reserve for replacement and quarantine, then fail fast when capacity is exhausted. Starting more workers than there are eligible accounts does not create parallelism; it creates setup contention. Track lease wait time, active leases by role, expired leases, and quarantined accounts so pipeline owners can distinguish an undersized pool from cleanup defects without exposing credentials.

Survive Failure, Retry, and Process Loss

Fixture teardown runs in normal completion and many failure paths, but it cannot run after every machine termination or forced process kill. Leases therefore need a TTL and a sweeper keyed by run ID. A heartbeat can extend long worker sessions; without one, set the TTL above the maximum expected worker duration and renew at safe points.

On retry after a worker restart, the stable owner should reacquire its existing valid lease. Before reuse, the service can reset known mutable fields or reject a lease marked unhealthy. If authentication was invalidated by the failed test, quarantine the account and allocate another rather than handing corrupted state to later tests.

Keep run-level cleanup independent from worker teardown. A final CI job can release all leases for QA_RUN_ID, even when some test processes crashed. That job must not release leases belonging to another pipeline.

Diagnose Pool Failures With Lease Evidence

Attach non-secret lease metadata to failed tests: lease ID, account ID, owner, role, expiry, and project. These fields reveal whether the failure is capacity, collision, stale state, or authentication.

SymptomLikely causeInvestigation
Two workers change the same settingsLease assignment is not atomic or owner keys collideQuery active leases by account and owner
Pool exhausts after retriesOld workers use new owner identities or leases never expireCompare worker and parallel indexes plus TTL
Tests fail only across projectsProject name is missing from owner or pool partitionInspect owner keys and role allocation
Fresh context sees prior recordsServer state, not browser state, leakedReview reset and per-test resource namespace
Sign-in fails midway through runCredential rotation or account quarantineRecord account health without logging secrets
Cleanup affects another jobRun identity is absent or reusedAudit the release query's ownership boundary

Do not automatically retry lease exhaustion for minutes. Report requested role, available capacity, and active owner count so infrastructure can be sized or leaked leases can be corrected.

Operational Checklist for Account Pools

  • Use one exclusive account lease per concurrent worker and role.
  • Build owner identity from run, shard, project, and parallelIndex.
  • Make lease acquisition and release atomic and idempotent.
  • Add TTL expiration plus run-level cleanup for killed workers.
  • Authenticate at worker scope but create a fresh context per test.
  • Reset or namespace server-side mutations between tests.
  • Keep storage state and credentials out of source control and logs.
  • Quarantine accounts whose state or authentication becomes unreliable.
  • Attach non-secret lease metadata to failures and capacity errors.

Rollout Plan

Begin with one stateful parallel project and measure its maximum concurrent workers by role. Provision a small surplus, implement an owner-aware lease endpoint, and replace shared credentials with a worker fixture. Add fresh per-test contexts and explicit cleanup for the account fields that suite mutates. Then simulate a failed test, worker replacement, and killed process. The pool is ready only when replacement reuses or safely replaces the lease, no other worker receives the same account, and run-level cleanup restores capacity without touching another run.

// 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 11, 2026 / Reviewed July 11, 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
    Playwright documentation

    Microsoft

    Canonical API, locator, fixture, browser, and test-runner behavior.

  2. 02
    Playwright best practices

    Microsoft

    Official guidance for resilient tests, isolation, and user-facing locators.

  3. 03
    WebDriver standard

    W3C

    The browser automation protocol specification used by major automation stacks.

FAQ / QUICK ANSWERS

Questions testers ask

Why use a worker-scoped account fixture in Playwright?

Account creation or leasing can be expensive, and one worker runs tests sequentially. A worker-scoped fixture amortizes that setup while giving concurrent workers different server-side identities.

Should an account pool use workerIndex or parallelIndex?

parallelIndex is stable when Playwright replaces a failed worker, while workerIndex changes for the new process. Include a run or shard identity too, because parallel indexes are local to a runner invocation.

Can tests in one worker safely share the same account?

Only if each test isolates or resets the server-side records it modifies. Give every test a fresh browser context and unique resource namespace even when the account lease is shared.

How should account leases be cleaned up after a crashed worker?

Fixture teardown should release normal leases, but the lease service also needs expiration and a run-level sweeper because a killed process may never execute teardown. Release operations should be idempotent.

Is it safe to store authenticated Playwright state for pooled accounts?

Keep storage state in memory or an ignored, access-controlled artifact location and never commit it. The state can contain cookies or headers that allow account impersonation.