Playwright in Pictures is a series of articles where I use playwright-timeline-reporter to visualize different Playwright concepts with simple timeline charts.
Playwright runs tests in workers. These are separate OS processes started by the test runner. Playwright reuses a worker while it can, but under some conditions it restarts the worker.
In this post I demonstrate all the cases where Playwright triggers a worker restart. This matters because the restart takes time, and more importantly, all worker-level hooks and fixtures run again after the restart, increasing the total execution time.
Example Setup
I use one test file with three tests:
import { expect, test } from '@playwright/test';
const wait = (ms: number) => new Promise(r => setTimeout(r, ms));
test('first', async () => {
await wait(1000);
});
test('second', async () => {
await wait(1000);
});
test('third', async () => {
await wait(1000);
});
The run uses a single worker:
npx playwright test --workers 1
This example run has no restart. All three tests run in the same worker.
Restart After Test Failure
Change the second test to fail:
test('second', async () => {
await wait(1000);
throw new Error('test error');
});
Playwright does not run the third test in the same worker. After a failure, it discards the worker process and starts a new one for the next test.
The restart gap is visible even without any setup hook. The third test waits for the replacement worker before it starts.
This behavior is documented in Playwright's retries guide: a failed test causes the whole worker process to be discarded, and the next test continues in a new worker.
The Hidden Cost: beforeAll Runs Again
Add a heavy beforeAll hook to the same file:
test.beforeAll(async () => {
await wait(2000);
});
Run the same failing test again.
beforeAll runs twice (live report ↗)The failed test no longer adds only worker restart gap. The run also contains a second execution of beforeAll (yellow bar).
Worker-scoped fixtures have the same cost. Playwright sets them up for each worker process, so a replacement worker initializes them again.
Some time ago I opened a Playwright issue for an option to keep a worker after test failure. It comes with trade-offs, but can improve test performance.
Restart on Project Switch
Another reason for a worker restart is a project boundary. When the config has multiple projects, Playwright restarts the worker when the next test belongs to a different project than the previous one.
I adapted the example to run all three tests in two projects:
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'project-a' },
{ name: 'project-b' },
],
});
Run with a single worker:
npx playwright test --workers 1
Timeline:
The timeline shows a worker restart gap, but this is not failure recovery. The first worker is used for project-a, and the second worker is used for project-b.
Playwright's projects guide describes projects as logical groups of tests that run with the same configuration. The timeline makes the process boundary visible.
Project Switch Repeats beforeAll
Add the same slow beforeAll hook back to the file:
test.beforeAll(async () => {
await wait(2000);
});
Both projects now pay the setup cost.
beforeAll runs twice (live report ↗)The setup is not shared across projects. Two projects mean two worker processes, and each worker runs its own beforeAll.
Restart on test.use()
There is another, less obvious case where a worker restarts. If a test file calls test.use() with a worker-scoped fixture, Playwright runs that file in a new worker.
To illustrate this, I split the three tests into two files. The first file, spec1.test.ts, contains two tests:
test('first', async () => {
await wait(1000);
});
test('second', async () => {
await wait(1000);
});
The second file, spec2.test.ts, contains the remaining test and overrides a worker-scoped fixture using test.use():
test.use({ screenshot: 'on' });
test('third', async () => {
await wait(1000);
});
Run with a single worker:
npx playwright test --workers 1
The timeline shows a worker restart, even though there are no test failures or project boundaries:
test.use() triggers worker restart (live report ↗)Although this case may seem uncommon, in practice any override of a worker-scoped fixture via test.use() causes a worker restart, which can increase the total test duration.
Key Takeaways
- Playwright restarts the worker after every test failure.
- A worker restart takes time to spin a new OS process.
- The larger impact is repeated execution of worker-level hooks and fixtures.
- Project boundaries also create new worker processes, so each project pays its own worker-level setup cost.
- Overriding worker-scoped fixtures via
test.use()triggers a worker restart.
Thanks for reading ❤️
👉 Prev in the series: Playwright in Pictures: Fully Parallel Mode






Top comments (0)