Advanced Chapter 15 · 12 min read

Parallel Execution & Sharding

Run thousands of tests faster by executing them in parallel across workers and sharding across CI machines.

How Playwright Parallelises Tests

By default, Playwright Test runs test files in parallel using a pool of worker processes, where each worker has its own browser instance. Tests within a single file run serially by default to preserve shared state. Enabling fullyParallel: true in the config also runs individual tests within a file in parallel, which is safe when each test is fully independent (the recommended design).

Tip: Avoid shared mutable state between tests — no global variables, no shared database rows without isolation. Each test should arrange its own data via API calls or fixtures so it can run in any order or in parallel without conflicts.

Configuring Workers

The workers option controls how many parallel processes run simultaneously. On a developer machine, leaving it as undefined lets Playwright auto-detect (typically half the CPU count). In CI, set it explicitly — for example 4 — to match the available runners. Saturating all CPU cores rarely helps because browser tests are I/O-bound, not CPU-bound.

Sharding Across CI Machines

Sharding splits the test suite across multiple CI machines or jobs. Pass --shard=1/3 to run the first third of tests, --shard=2/3 for the second, and so on. After all shards complete, merge the blob reports with npx playwright merge-reports to get a single unified HTML report. Pytest users achieve equivalent parallelism with pytest-xdist and the -n auto flag.

playwright.config.parallel.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Run every test in every file in parallel
  fullyParallel: true,

  // 4 workers in CI, auto in local dev
  workers: process.env.CI ? 4 : undefined,

  // Merge-able blob reports for sharding
  reporter: process.env.CI
    ? [[ 'blob' ], [ 'github' ]]
    : [[ 'html', { open: 'on-failure' } ]],

  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
});

/* Run commands:
   Local:   npx playwright test
   Shard 1: npx playwright test --shard=1/3
   Shard 2: npx playwright test --shard=2/3
   Shard 3: npx playwright test --shard=3/3
   Merge:   npx playwright merge-reports --reporter html ./all-blob-reports
*/
tests/parallel-safe.spec.js
import { test, expect } from '@playwright/test';

// These tests are safe to run in parallel because
// each one creates and deletes its own isolated data

test('create order A', async ({ request, page }) => {
  const { id } = await (await request.post('/api/orders', {
    data: { product: 'Widget A' }
  })).json();

  await page.goto(`/orders/${id}`);
  await expect(page.getByText('Widget A')).toBeVisible();
  await request.delete(`/api/orders/${id}`);
});

test('create order B', async ({ request, page }) => {
  const { id } = await (await request.post('/api/orders', {
    data: { product: 'Widget B' }
  })).json();

  await page.goto(`/orders/${id}`);
  await expect(page.getByText('Widget B')).toBeVisible();
  await request.delete(`/api/orders/${id}`);
});
ParallelTest.java
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.parallel.*;

// Enable parallel execution in junit-platform.properties:
// junit.jupiter.execution.parallel.enabled=true
// junit.jupiter.execution.parallel.mode.default=concurrent

@Execution(ExecutionMode.CONCURRENT)
class ParallelTest extends BaseTest {

  @Test
  void orderA() {
    page.navigate("/orders/create");
    page.getByLabel("Product").fill("Widget A");
    page.getByRole(AriaRole.BUTTON,
        new Page.GetByRoleOptions().setName("Place Order")).click();
  }

  @Test
  void orderB() {
    page.navigate("/orders/create");
    page.getByLabel("Product").fill("Widget B");
    page.getByRole(AriaRole.BUTTON,
        new Page.GetByRoleOptions().setName("Place Order")).click();
  }
}
pytest.ini
# pytest.ini — enable xdist parallel execution
[pytest]
addopts = -n auto --dist=loadscope

# Install: pip install pytest-xdist
# Run:     pytest -n 4          (4 workers)
# Run:     pytest -n auto       (auto-detect CPUs)
test_parallel_safe.py
import pytest
from playwright.sync_api import APIRequestContext, Page, expect

@pytest.mark.parametrize("product", ["Widget A", "Widget B", "Widget C"])
def test_create_order(
    product: str,
    api_context: APIRequestContext,
    page: Page
):
    # Each parametrized test is isolated by its own order
    resp = api_context.post("/api/orders", data={"product": product})
    order_id = resp.json()["id"]

    page.goto(f"/orders/{order_id}")
    expect(page.get_by_text(product)).to_be_visible()

    api_context.delete(f"/api/orders/{order_id}")

Playwright Advanced Parallel Execution & Sharding

Written by PV

© 2026 All Rights Reserved