Q29 of 42 · Playwright
How does Playwright run tests in parallel across workers, and what should be worker-scoped vs test-scoped?
Short answer
Short answer: Each worker is a Node process with its own browser. By default tests in the same file run serially in a worker; tests in different files run in parallel across workers. Test-scope expensive setup belongs in test fixtures; worker-scope shared setup (DB seeds, slow auth) belongs in worker fixtures. Match scope to teardown cost and isolation needs.
Detail
The execution model:
- Playwright spawns N worker processes (
workers: N). - Each worker is a separate Node process with its own browser instance and context.
- The runner distributes spec files (or test parallel groups within files) to idle workers.
- Within a file, tests run serially by default.
test.describe.parallelopts a group into intra-file parallelism.
Fixture scopes:
scope: 'test'(default): the fixture's setup/teardown runs once per test.page,contextare test-scoped — fresh isolation per test.scope: 'worker': setup/teardown runs once per worker, regardless of how many tests it processes. Used for expensive shared resources.
What belongs where:
Test-scoped (most fixtures):
- Anything that needs isolation: a new browser context, a temp directory, a transactional DB session.
- Fixtures that depend on test inputs.
- Anything cheap to recreate.
Worker-scoped (sparingly):
- Database seed: insert canonical fixture rows once per worker; tests read but don't modify.
- Auth state: log in once per worker, share the storage state.
- Containers / external services: spin up a Docker dependency once per worker.
- Anything that takes >2 seconds to set up and is genuinely shareable.
Pitfalls:
- Mutating worker-scoped state. If two tests in the same worker both modify the same shared resource, you have ordering coupling. Either keep worker resources read-only or use test-scoped state.
- Cross-worker assumptions. Worker A's seed isn't visible to worker B's worker fixture; each runs setup independently. If you need true cross-worker shared state, use
globalSetup(runs once before all workers).
globalSetup / globalTeardown: runs once before/after the entire run, across all workers. Right place for things like spinning up a Docker compose stack, running DB migrations, or one-time cleanup.
Typical scope hierarchy:
globalSetup (1× per run) — start docker, run migrations
worker fixture (1× per worker) — log in, seed read-only data
test fixture (1× per test) — fresh page, transactional DB session
The senior signal: knowing the scope ladder, naming globalSetup vs worker fixture, and the mutating-shared-state trap.