PRACTICAL GUIDE / Playwright beforeAll retry behavior
Why beforeAll runs again when Playwright retries a test
Understand Playwright worker restarts, make beforeAll setup safe to repeat, and keep retry attempts from sharing or leaking external test data.
In this guide6 sections
What you will learn
- A retry starts with a new worker
- Reproduce the lifecycle with a runnable test
- Find out whether setup or the test really failed
- Make repeated setup an explicit design choice
Your test creates an account in beforeAll, fails halfway through, and then the retry gets a duplicate-account response. The hook did not run once for the command; it ran once in each worker that handled the suite.
That behavior is deliberate: Playwright replaces a worker after a test failure so contaminated browser and process state cannot leak into the next attempt. Any setup tied to that worker must be safe to run again.
A retry starts with a new worker
Playwright Test executes test files in worker processes. A worker owns its browser and the JavaScript module instance that contains variables declared by the test file. While tests pass, the worker may continue running more tests from the file.
Once a test fails, Playwright discards that worker and starts another one. With retries enabled, the new worker begins by running the failed test again. Before it can do that, it evaluates the test file and executes the applicable beforeAll hook. The old value of a module variable is not carried into the new process.
The documented sequence for a normal failure is beforeAll, tests, afterAll, then a fresh worker with another beforeAll. That sequence explains two bugs that often look unrelated:
- A creation hook fails on retry because the first attempt left an external record behind.
- A hook is skipped on retry, but the test crashes because the new worker has none of the in-memory state created by the first hook.
testInfo.retry identifies the attempt. It is 0 for the initial run, 1 for the first retry, and so on. testInfo.workerIndex identifies a particular worker process and changes after a restart. testInfo.parallelIndex identifies the parallel slot and remains the same when Playwright replaces that worker.
Those values are identifiers, not a data strategy. A resource named only with workerIndex avoids collisions between process attempts but can leave one resource per failed worker. A resource named only with parallelIndex can be reused after a restart, but setup must be an idempotent create-or-update operation. In CI, include a run identifier and project name as well, or separate jobs can collide.
Retries also change the meaning of a passing report. A test that failed first and passed later is classified as flaky, not cleanly passed. Hiding the first attempt's setup failure with broad catch blocks removes the evidence the retry system is meant to preserve.
Reproduce the lifecycle with a runnable test
This self-contained TypeScript test writes a small setup record into the project's output directory. The first attempt fails only when DEMO_RETRY=1 is set. On retry, a new worker runs beforeAll, overwrites the slot's record, and the test verifies that the file belongs to the current worker.
import { expect, test } from '@playwright/test';
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import * as path from 'node:path';
type SetupRecord = {
retry: number;
workerIndex: number;
parallelIndex: number;
};
test.describe('retry-safe beforeAll', () => {
let recordPath = '';
test.beforeAll(async ({}, testInfo) => {
const project = testInfo.project.name.replace(/\W+/g, '_') || 'default';
const directory = path.join(
testInfo.project.outputDir,
'before-all-retry-demo',
);
// parallelIndex is stable when Playwright replaces a failed worker.
recordPath = path.join(
directory,
`${project}-slot-${testInfo.parallelIndex}.json`,
);
const record: SetupRecord = {
retry: testInfo.retry,
workerIndex: testInfo.workerIndex,
parallelIndex: testInfo.parallelIndex,
};
await mkdir(directory, { recursive: true });
await writeFile(recordPath, JSON.stringify(record, null, 2), 'utf8');
console.log(
`beforeAll retry=${record.retry} worker=${record.workerIndex} ` +
`slot=${record.parallelIndex}`,
);
});
test.afterAll(async () => {
if (recordPath) {
await rm(recordPath, { force: true });
}
});
test('uses setup from the current worker', async ({}, testInfo) => {
const record = JSON.parse(
await readFile(recordPath, 'utf8'),
) as SetupRecord;
expect(record.retry).toBe(testInfo.retry);
expect(record.workerIndex).toBe(testInfo.workerIndex);
expect(record.parallelIndex).toBe(testInfo.parallelIndex);
if (process.env.DEMO_RETRY === '1') {
// Attempt 0 fails; retry 1 passes.
expect(testInfo.retry).toBe(1);
}
});
});Save it as tests/before-all-retry.spec.ts, then run:
DEMO_RETRY=1 npx playwright test tests/before-all-retry.spec.ts \
--retries=1 --workers=1 --reporter=line --trace=onThe console should show two beforeAll lines. Their retry and worker numbers change, while the parallel slot remains stable. The final result is flaky because it passed only after a retry. Remove DEMO_RETRY=1 and the same test passes on its first attempt.
The file operation stands in for an external API or database call. writeFile is repeatable for a stable path. A production equivalent might be an upsert keyed by CI run, Playwright project, and parallel slot. A plain create call with a random ID has a different lifecycle and requires that ID to be recovered for cleanup.
Find out whether setup or the test really failed
Run the smallest test file with --workers=1, one retry, and tracing enabled. This removes unrelated concurrency without changing the worker-restart rule. In the HTML report or trace, compare the failed attempt with the retry rather than opening only the green result.
Check the hook steps first. Did beforeAll complete on both attempts? If the retry failed before the test body started, the setup is the primary failure. A screenshot of the application taken later cannot explain it.
Next, log or attach the identity of every external resource created by setup. Include retry, workerIndex, parallelIndex, project, and the CI run identifier. Match those values with API or database audit records. A 409 Conflict on retry usually means setup repeated a non-idempotent create. A 404 Not Found in the test often means teardown from the previous worker deleted a resource that the new worker expected to reuse.
Inspect the first attempt's afterAll result too. Playwright normally runs it after a test failure, as shown in the documented retry sequence. If cleanup throws, that error can add noise or obscure the original failure. Preserve both records and make deletion accept an already-absent resource when absence is the desired final state.
For hard crashes, machine loss, or a canceled CI job, there may be no completed teardown step. Look for a log line proving cleanup finished, not merely one proving it started. Resources with a time-to-live or a scheduled cleanup job are easier to operate than resources whose only exit path is afterAll.
Finally, ask whether later tests depend on mutations made by earlier tests. If they do, a retry may encounter a state no hook can reliably reconstruct. Playwright's isolation model works best when each test can start from declared setup and run independently.
Make repeated setup an explicit design choice
The safest pattern is idempotent provisioning. Choose a stable key for the resource, then make beforeAll create it if absent or reset it to a known state if present. The operation must verify the returned record rather than treating any successful HTTP status as enough.
Stable naming has a cost. Parallel jobs, shards, browsers, and repeated CI runs need disambiguation. A useful key often includes the CI run ID, project name, and parallelIndex. Do not use parallelIndex by itself in a shared environment because separate commands can both have slot zero.
Per-test setup is the stronger option when tests mutate the resource. Move creation into beforeEach or a test-scoped fixture and give every test a unique record. Retries then receive fresh data. The trade-off is more API traffic and longer execution, but failures are easier to attribute and parallelism is safer.
A worker-scoped fixture can replace a large beforeAll when multiple tests genuinely share an expensive, read-only resource. It still starts again when the worker restarts. Its advantage is explicit scope and composable teardown, not immunity from retries.
Global setup is suitable for bootstrapping an environment that truly belongs to the whole command, such as checking a service or preparing a shared immutable dataset. Moving mutable test accounts there merely enlarges the sharing boundary. One test can then damage data used by every worker.
Whichever scope you choose, make cleanup idempotent. Deleting an absent test record should normally count as success. Add expiry metadata when the backing system supports it, and run a janitor keyed to old CI run IDs. That extra machinery is the price of surviving process termination rather than only ordinary assertion failures.
Avoid the retry shortcuts that create worse failures
Do not write if (testInfo.retry > 0) return at the top of beforeAll. The new worker does not inherit the old worker's variables, browser context, open handles, or authenticated page. Skipping setup leaves the retry with partial external state and no local reference to it.
Do not catch a duplicate error and continue without loading and validating the existing record. The duplicate might belong to another job, contain mutations from the failed attempt, or use an incompatible schema. Idempotence means converging on a verified state, not ignoring conflict.
Serial mode is not a repair for shared setup. In a serial group, a failure causes the group to be retried together, so earlier tests can run again as well. This expands the amount of repeated work and reinforces test dependencies.
Avoid using retries to validate the hook itself in the main suite. Keep a small lifecycle test, like the example above, for framework behavior. Product tests should not intentionally fail their first attempt because that trains the team to accept flaky output.
Keep beforeAll when the scope is genuinely shared
There is nothing inherently wrong with beforeAll. It is a good fit for immutable data loaded once per worker, a local mock server whose lifetime is the worker, or an expensive read-only artifact used by independent tests. The setup still needs repeatable startup and reliable teardown.
Do not move a cheap, test-specific prerequisite into the hook merely to save seconds. The speed gain buys coupling: one setup failure skips or fails several tests, and a retry repeats the shared boundary. Isolation is usually worth a small runtime cost.
Conversely, do not create a new resource per test when the upstream system has strict quotas and the tests only read it. A verified worker-scoped fixture may be the responsible choice. State that ownership in code and monitor resource counts so leakage is visible.
The deciding question is not whether beforeAll runs again. It will whenever a relevant new worker starts. Decide what may be repeated, what may be shared, and how an abandoned attempt is cleaned up. Once those answers are explicit, retries stop being surprising and start doing their real job: exposing an unreliable first attempt.
// 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.
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.
- 01Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Does Playwright run beforeAll again on a retry?
Yes. A test failure causes Playwright Test to discard that worker process, and the retry starts in a new worker where the relevant beforeAll hook runs again.
How can a beforeAll hook tell which retry is running?
Read testInfo.retry in the hook. The first attempt is zero, the first retry is one, and later retries continue increasing.
Should I skip beforeAll when testInfo.retry is greater than zero?
Skipping setup is usually wrong because the retry runs in a fresh worker and module variables from the old process are gone. Make setup repeatable or reload the required state instead.
What is the difference between workerIndex and parallelIndex?
A restarted worker gets a new workerIndex but retains the same parallelIndex. Use either only after deciding whether your external resource belongs to one process attempt or to a stable parallel slot.
Will afterAll always clean resources from a failed attempt?
Normal test failures follow Playwright's teardown sequence, but a killed process or canceled job can still prevent cleanup. External resources need idempotent deletion, expiry, or a separate janitor rather than absolute trust in one hook.
RELATED GUIDES
Continue the learning route
GUIDE 01
Classify Flaky, Expected, and Failed Tests with Playwright Retries
Use Playwright retries, annotations, worker behavior, and result evidence to distinguish flaky tests, expected failures, and real regressions in CI.
GUIDE 02
Test Canonical URLs with Playwright
Build Playwright test canonical URLs checks for rendered link tags, absolute hrefs, redirect variants, indexable routes, and metadata regressions in CI.
GUIDE 03
Test JSON-LD Schema with Playwright
Learn Playwright test JSON-LD schema checks for BlogPosting, FAQPage, and BreadcrumbList, including parsing, required fields, URLs, and CI failures.
GUIDE 04
Test Reduced Motion with Playwright
Use Playwright reduced motion testing with media emulation to verify static alternatives, disabled animations, usable content, and regression checks in CI.
GUIDE 05
Test XML Sitemaps with Playwright
Use Playwright test XML sitemap checks to validate status, content type, canonical host, duplicate URLs, discoverability, and protocol rules in CI.