DEV Community

Cover image for Playwright in Pictures: Why Workers Restart
Vitaliy Potapov
Vitaliy Potapov

Posted on

Playwright in Pictures: Why Workers Restart

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 worker processes. 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);
});
Enter fullscreen mode Exit fullscreen mode

The run uses a single worker:

npx playwright test --workers 1
Enter fullscreen mode Exit fullscreen mode

This example run has no restart. All three tests run in the same worker.

All tests pass in the same worker

All tests run in the same worker (live report ↗)

Restart After Test Failure

Change the second test to fail:

test('second', async () => {
  await wait(1000);
  throw new Error('test error');
});
Enter fullscreen mode Exit fullscreen mode

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.

A failed test ends Worker 1; Worker 2 starts before the remaining test

A failed test ends Worker 1; Worker 2 starts before the remaining test (live report ↗)

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);
});
Enter fullscreen mode Exit fullscreen mode

Run the same failing test again.

beforeAll runs again in the replacement worker

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' },
  ],
});
Enter fullscreen mode Exit fullscreen mode

Run with a single worker:

npx playwright test --workers 1
Enter fullscreen mode Exit fullscreen mode

Timeline:

The second project starts in a new worker

The second project starts in a new worker (live report ↗)

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);
});
Enter fullscreen mode Exit fullscreen mode

Both projects now pay the setup cost.

beforeAll runs once per project worker

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.

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.

Thanks for reading ❤️

Top comments (0)