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.
In this guide9 sections
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.
01 / worker identity
Worker identity
Combine run, shard, project, and parallel index into one owner.
02 / account lease
Account lease
Atomically reserve one eligible account for that owner.
03 / authenticated context
Authenticated context
Bootstrap fresh per-test browser state from worker authentication.
04 / isolated test batch
Isolated test batch
Namespace records and reset shared server state between tests.
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.
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.
// 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.
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.
| Symptom | Likely cause | Investigation |
|---|---|---|
| Two workers change the same settings | Lease assignment is not atomic or owner keys collide | Query active leases by account and owner |
| Pool exhausts after retries | Old workers use new owner identities or leases never expire | Compare worker and parallel indexes plus TTL |
| Tests fail only across projects | Project name is missing from owner or pool partition | Inspect owner keys and role allocation |
| Fresh context sees prior records | Server state, not browser state, leaked | Review reset and per-test resource namespace |
| Sign-in fails midway through run | Credential rotation or account quarantine | Record account health without logging secrets |
| Cleanup affects another job | Run identity is absent or reused | Audit 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.
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.
- 01Playwright documentation
Microsoft
Canonical API, locator, fixture, browser, and test-runner behavior.
- 02Playwright best practices
Microsoft
Official guidance for resilient tests, isolation, and user-facing locators.
- 03
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.
RELATED GUIDES
Continue the learning route
GUIDE 01
How to Run Tests in Parallel with Playwright
Learn how to run tests in parallel with Playwright: workers, sharding, isolation, CI config, flakiness fixes, and a practical checklist for faster suites.
GUIDE 02
Playwright Fixtures Explained: Test Setup That Scales
Playwright fixtures explained with test scope, worker scope, custom setup, cleanup, auth state, examples, reporting, and mistakes to avoid in CI.
GUIDE 03
API Authentication Testing: OAuth, JWT, and API Keys
Learn API authentication testing for OAuth 2.0, JWT expiry and refresh flows, API keys vs bearer tokens, and broken authentication test cases.
GUIDE 04
Flaky Tests: Causes and How to Fix Them
Learn how to fix flaky tests with root cause analysis, stable waits, quarantine strategy, CI retries policy, and practical Playwright examples.