Skip to main content
Back to Blog
Guide
2026-05-10

Playwright Parallel and Sharded Execution: Complete 2026 Guide

Speed up Playwright tests with fullyParallel, workers, sharding, and CI matrices. Patterns for isolation, data partitioning, and report merging in 2026.

Playwright Parallel and Sharded Execution: Complete 2026 Guide

A 400-test suite that runs serially takes thirty minutes. The same suite, fully parallel across four workers and three CI shards, finishes in under three minutes. The performance gap is not just about CPUs; it is about the difference between a CI pipeline developers wait for and one they route around. Playwright's parallelism model gives you both worker-level concurrency inside a single process and cross-machine sharding via --shard. Combine them and the speedup is multiplicative.

This guide covers everything you need to run Playwright tests in parallel and across shards in 2026: workers, fullyParallel, data isolation, sharding strategies, CI matrices, and the report-merging step that pulls shard outputs back together. Every example is TypeScript with Playwright 1.49+.

For broader CI scaffolding, the Playwright CI GitHub Actions Complete Guide covers the full pipeline. The playwright-e2e skill bakes in these patterns for AI-generated tests.

The two axes of parallelism

Playwright parallelism has two independent axes:

AxisMechanismScope
Workersworkers config optionSame machine
Shards--shard CLI flagMultiple machines

Workers are CPU-bound; you typically run one worker per CPU core. Shards are machine-bound; you run one shard per CI runner. A 4-worker, 3-shard matrix gives you 12-way parallelism.

fullyParallel

By default, Playwright runs files in parallel and tests within a file sequentially. fullyParallel: true enables test-level parallelism, which is what you want for any suite where individual tests are independent.

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

export default defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 4 : undefined,
});

When workers is omitted, Playwright picks roughly half of available cores. process.env.CI ? 4 : undefined keeps local runs flexible while pinning CI to a deterministic number.

Disabling parallelism per file

When tests within a file genuinely share state (browser session, generated data), keep them serial.

test.describe.configure({ mode: 'serial' });

test.describe('Checkout flow', () => {
  test('add item', async () => { ... });
  test('view cart', async () => { ... });
  test('place order', async () => { ... });
});

mode: 'serial' runs tests in declaration order within one worker and fails subsequent tests on the first failure. Use sparingly; refactor toward independence whenever possible.

mode: 'default' runs in declaration order but in parallel within the file (with fullyParallel: true set).

Data isolation per worker

The cardinal rule of parallel tests: no shared mutable state. Two workers writing to the same database row or filesystem path will race.

Use testInfo.workerIndex to partition data:

import { test as base } from '@playwright/test';

type Fixtures = {
  testUser: { id: string; email: string };
};

export const test = base.extend<Fixtures>({
  testUser: async ({}, use, workerInfo) => {
    const email = `user-w${workerInfo.workerIndex}-${Date.now()}@example.com`;
    const user = await db.createUser({ email });
    await use(user);
    await db.deleteUser(user.id);
  },
});

Each worker gets its own user with a unique email derived from workerIndex.

Sharding

To run tests across multiple machines, pass --shard=index/total.

# Run shard 1 of 4
npx playwright test --shard=1/4

# Run shard 2 of 4
npx playwright test --shard=2/4

Playwright splits the test list deterministically. Combined with workers, this gives you (shards * workers) total parallelism. A typical config for a 400-test suite:

strategy:
  fail-fast: false
  matrix:
    shardIndex: [1, 2, 3, 4]
    shardTotal: [4]
runs-on: ubuntu-latest
steps:
  - run: pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

Four shards times four workers = 16 parallel tests, finishing 400 in roughly 25-30 minutes of CPU time spread across 4 minutes wall clock.

The blob reporter for shard merging

Each shard produces its own report. To merge them, use the blob reporter on every shard, then a separate job to merge.

// playwright.config.ts
reporter: process.env.CI ? [['blob']] : [['list']],

Each shard writes blob-report/ with serialized results. Upload as artifact:

- uses: actions/upload-artifact@v4
  with:
    name: blob-report-${{ matrix.shardIndex }}
    path: blob-report

Then a merge job:

merge-reports:
  needs: [test]
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/download-artifact@v4
      with:
        pattern: blob-report-*
        merge-multiple: true
        path: all-blob-reports
    - run: pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
    - uses: actions/upload-artifact@v4
      with:
        name: html-report
        path: playwright-report

The merged HTML report includes every test from every shard in a single browsable file.

Cross-browser matrix

For broader coverage, add browser to the matrix.

strategy:
  fail-fast: false
  matrix:
    browser: [chromium, firefox, webkit]
    shardIndex: [1, 2]
    shardTotal: [2]

Six runners (two shards times three browsers) cover a 400-test suite across browsers in ~5 minutes.

Project-level parallelism

Projects also run in parallel within a single shard.

projects: [
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
  { name: 'webkit', use: { ...devices['Desktop Safari'] } },
],

With fullyParallel: true and three projects, your test pool triples. Workers distribute the union of (test, project) tuples.

Sequential setup project

Some tests require an initial setup (authentication, database seeding) that should run once before parallel tests.

projects: [
  {
    name: 'setup',
    testMatch: /.*\.setup\.ts/,
  },
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],
  },
],

Setup runs to completion before any test in the dependent projects. Setup itself can be parallel internally; the dependency only enforces ordering between projects.

Failing-fast vs failing-slow

fail-fast: false in the GitHub matrix lets every shard finish even if one fails. The tradeoff: you get full visibility into all failures, but you pay for the runners. fail-fast: true cancels remaining runners on first failure, saving cost at the expense of context.

For most teams, fail-fast: false is correct because diagnosing two failures simultaneously saves more developer time than the extra minutes of runner cost.

Test discovery and grep

To run a subset:

# Only smoke tests
npx playwright test --grep "@smoke"

# Exclude flaky tests
npx playwright test --grep-invert "@flaky"

# Specific file
npx playwright test tests/checkout.spec.ts

# Specific test
npx playwright test -g "user completes checkout"

In CI, combine with shards for partitioned runs:

pnpm exec playwright test --grep "@smoke" --shard=1/2

Worker isolation patterns

ResourceIsolation strategy
DatabasePer-worker schema or table prefix
FilesystemUse testInfo.outputDir (auto-unique)
External APITag requests with worker ID header
Local server portAllocate dynamically per worker
Time-sensitive stateUse page.clock (see Playwright Clock Time Control Testing Guide)

Allocating ports per worker

For each worker that needs its own backend port:

import { test as base } from '@playwright/test';

export const test = base.extend<{ port: number }>({
  port: async ({}, use, workerInfo) => {
    const port = 4000 + workerInfo.workerIndex;
    await spawnBackend(port);
    await use(port);
  },
});

Sharding strategy options

Playwright's default sharding splits the test list into N contiguous chunks. For more even balance with long-running tests, sort tests by historical duration first. Playwright 1.49+ supports duration-aware sharding by including --shard with results from previous runs:

npx playwright test --shard=1/4 --last-failed

--last-failed runs only the tests that failed in the previous run, useful for fast retry workflows.

Common pitfalls

Pitfall 1: Shared mutable fixtures at worker scope without cleanup. A test that fails mid-fixture leaves leftover state for the next test.

Pitfall 2: Database collisions. Two workers using the same row crash. Use unique data per worker.

Pitfall 3: Forgetting to upload blob artifacts. Without blobs, shards cannot merge into a single report.

Pitfall 4: workers: 1 in CI. A single worker defeats parallelism. Use workers: 4 or higher in CI.

Pitfall 5: fullyParallel: false by default. Many older configs forget to enable it; tests within a file run serially even though they could parallelize.

Anti-patterns

  • Sharing global counters between tests. Counters are not parallel-safe.
  • Hard-coding shard counts in test code. The CLI handles it.
  • Skipping the merge job. Without it, only the last shard's report survives.
  • Running every test in every project. Use testMatch to scope per project.

Putting it all together

name: Playwright

on: [pull_request]

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]
        shardIndex: [1, 2, 3]
        shardTotal: [3]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with:
          version: 9.15.0
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm exec playwright install --with-deps ${{ matrix.browser }}
      - run: pnpm exec playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: blob-${{ matrix.browser }}-${{ matrix.shardIndex }}
          path: blob-report

  merge:
    if: ${{ !cancelled() }}
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - uses: actions/download-artifact@v4
        with:
          pattern: blob-*
          merge-multiple: true
          path: all-blob-reports
      - run: pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
      - uses: actions/upload-artifact@v4
        with:
          name: html-report
          path: playwright-report

Conclusion and next steps

Parallelism is the lever that turns a slow Playwright suite into a fast one. Use fullyParallel for in-process parallelism, --shard for cross-machine parallelism, the blob reporter for merging, and worker-keyed data partitioning for isolation.

Install the playwright-e2e skill so AI assistants generate parallel-safe tests. For broader CI patterns, read Playwright CI GitHub Actions Complete Guide. For retries on top of parallelism, Playwright Retries Flaky Test Handling Guide.

Playwright Parallel and Sharded Execution: Complete 2026 Guide | QASkills.sh