A suite that takes forty minutes gets run once a day. A suite that takes four minutes gets run on every commit. Parallel execution is the difference, and Playwright does it by default, which is why understanding it early prevents confusing failures.
A worker is a separate process running tests. Playwright starts several and hands each a share of the work. Four workers means four tests running at the same instant, in four independent browser processes.
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? 4 : undefined,
});
workers: undefined locally means Playwright picks a number from your CPU cores. On CI it is fixed, so runs are predictable and do not fight the build machine.
fullyParallel: true runs tests inside a single file at the same time too. Without it, files run in parallel but the tests inside one file run in order.
Now the consequences, which is the part that matters.
Test order is not guaranteed, ever. Test 3 may finish before test 1 starts. Any suite that quietly relied on order breaks immediately. That is a feature: it surfaces the dependency now instead of on a bad day.
Every worker is fully separate. Its own browser process, its own memory. Nothing you set in a variable in one test is visible in another. Sharing state between tests is not merely bad practice here, it is impossible.
Your test data has to be unique. Two workers creating a contact called "Test User" at the same moment will collide. This is the rule from the test data lecture, and parallelism is what makes ignoring it fatal rather than occasional.
⚠️ The trap: a shared external resource.
Parallel tests are isolated from each other, but they all hit the same application and the same database. Two tests deleting all contacts at once will break each other no matter how isolated the browsers are.
If a test must not run alongside others, say so explicitly with
test.describe.serial, and treat every use of it as a small debt.
When you truly need sequence:
test.describe.serial('checkout flow', () => {
test('adds to cart', async ({ page }) => { /* ... */ });
test('completes payment', async ({ page }) => { /* ... */ });
});
These run in order in one worker, and if the first fails the rest are skipped. Use it sparingly. A serial block is a group of tests that cannot be sped up and that all fail together, which is most of the reasons you wanted tests in the first place.
Sharding across machines. For very large suites, split the run across several machines:
npx playwright test --shard=1/3
Each machine runs a third. This only works because tests are independent, which is the recurring theme of this whole track.
Remember this: workers are separate processes, order is never guaranteed, and unique data is what makes parallelism safe.
Reference: Parallelism.