Resource library

QA How-To

Playwright sharding across machines: Examples and Best Practices

Explore playwright sharding across machines examples for GitHub Actions, GitLab CI, containers, report merging, data isolation, and balanced execution.

20 min read | 2,537 words

TL;DR

Use a CI matrix to run npx playwright test --shard=current/total on equivalent machines. Upload one uniquely named blob report per job, merge all blobs downstream, and isolate test users and resources by parallelIndex or CI job identity.

Key Takeaways

  • The portable unit is one Playwright command with a unique 1-based current/total shard pair.
  • CI matrices should vary only the shard index while keeping code, config, browser image, and target identical.
  • Blob reports are the handoff format for combining attachments and results from distributed jobs.
  • Shard-aware account and artifact allocation prevents concurrency defects from masquerading as product bugs.
  • Containerized shards need the same Playwright package and browser image versions.
  • A balanced pipeline measures slowest-job duration, setup overhead, retry load, and environment saturation.

These Playwright sharding across machines examples show the complete mechanics that surround --shard=current/total: local proof, GitHub Actions, GitLab CI, container parity, blob report aggregation, worker-safe test accounts, and failure diagnostics. The syntax is small, but a copy-ready production pattern must preserve coverage and evidence across independent jobs.

Use this guide as a cookbook. Each example solves a specific delivery problem and includes the assumption that makes it safe. If you need the architecture and capacity-planning rationale first, read how to use Playwright sharding across machines.

TL;DR

For three independent machines, run one command on each:

npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3
Example Use it when Key safeguard
Local shell proof Validating discovery and filters Use --list before spending CI capacity
GitHub Actions matrix Repository uses Actions Unique 1-based matrix index
GitLab parallel job Repository uses GitLab CI Use its 1-based CI_NODE_INDEX
Docker shards Runner parity matters Match package and browser image versions
Worker fixture Tests mutate server data Allocate unique account per worker
Blob merge job One report is required Upload artifacts after failures

1. Validate Playwright sharding across machines examples locally

Do not begin by editing a large CI workflow. First prove that the suite discovers tests consistently and that the intended projects and filters are active. The --list option shows selection without executing test bodies:

npx playwright test --list --shard=1/2
npx playwright test --list --shard=2/2

Then execute the two parts in separate terminals:

CI=true npx playwright test --shard=1/2
CI=true npx playwright test --shard=2/2

These commands are a correctness check, not a performance benchmark if both terminals share one laptop. They contend for the same CPU, memory, browser cache, and application process. The purpose is to catch invalid indices, inconsistent filters, test discovery issues, and shared-data collisions early.

Use the same project selection that CI will use:

npx playwright test --project=chromium --grep=@regression --list --shard=1/3

Every shard must receive the same --project and --grep values. Only current changes. The index is 1-based, so a two-shard run uses 1/2 and 2/2, never 0/2.

Test count is a weak balance signal. One visual test may take much longer than several API-backed UI checks. Capture wall times after execution and note serial groups, beforeAll setup, and unusually large files. Those observations decide whether file-level distribution is sufficient or fullyParallel is worth enabling.

2. Use one shard-ready Playwright configuration

Avoid a separate configuration file per machine. One shared config prevents quiet differences in projects, retries, timeouts, and reporters:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: Boolean(process.env.CI),
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: process.env.CI
    ? [['blob'], ['line']]
    : [['html', { open: 'never' }]],
  use: {
    baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
    screenshot: 'only-on-failure',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

The line reporter gives readable job logs while blob supplies the mergeable artifact. An array of reporters is supported. Keep the blob output directory local to the CI job so simultaneous machines do not write to one network filesystem.

fullyParallel enables individual-test distribution, which usually gives the scheduler more units to balance. Remove it if the suite intentionally requires file-level behavior, then keep files small enough that one file does not monopolize a shard. Do not use the option as a cosmetic speed toggle. It is a test-independence contract.

Use an explicit CI worker count during tuning. If four shards each start two workers, the target sees about eight browser workers. A stable Playwright parallel testing strategy is a prerequisite for multiplying that load across machines.

3. GitHub Actions matrix example with four machines

This workflow creates four equivalent jobs and keeps running the matrix after one shard fails:

name: e2e

on:
  pull_request:
  workflow_dispatch:

jobs:
  playwright:
    name: pw ${{ matrix.shard }}/4
    runs-on: ubuntu-latest
    timeout-minutes: 40
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - name: Execute selected shard
        run: npx playwright test --project=chromium --shard=${{ matrix.shard }}/4
        env:
          CI: "true"
          BASE_URL: ${{ vars.QA_BASE_URL }}
          CI_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}
      - name: Publish blob
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@v4
        with:
          name: blob-${{ matrix.shard }}
          path: blob-report
          if-no-files-found: error
          retention-days: 1

The total appears in the job name and command. For a dynamic total, use one matrix field for the total and reference that field everywhere so labels and commands cannot drift. The run identity supports unique test data and artifact correlation.

if-no-files-found: error catches a missing blob rather than allowing the merge job to present an incomplete report. The artifact still uploads when tests fail because !cancelled() is true for normal failures.

Install only Chromium when the project runs only Chromium. If the config includes Firefox and WebKit, install the needed browsers or use the standard full installation command. The package lockfile remains the source of the Playwright version used on all jobs.

4. Merge GitHub Actions blob artifacts

Add a job that depends on the complete matrix. It should run after test failures but not pretend a manually cancelled workflow is complete:

  merge-playwright:
    name: combined Playwright report
    if: ${{ !cancelled() }}
    needs: [playwright]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - uses: actions/download-artifact@v5
        with:
          pattern: blob-*
          path: all-blobs
          merge-multiple: true
      - name: Merge four shards
        run: npx playwright merge-reports --reporter=html ./all-blobs
      - uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report
          if-no-files-found: error
          retention-days: 14

The merge command reads blob files, not four generated HTML folders. HTML is a presentation format, while blob is the distributed handoff format. The downstream job installs dependencies from the same revision so the merging Playwright version matches.

If CI requires JUnit and HTML, create a small merge configuration:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [
    ['html', { open: 'never' }],
    ['junit', { outputFile: 'test-results/e2e.xml' }],
  ],
});
npx playwright merge-reports --config=merge.config.ts ./all-blobs

Upload both outputs with appropriate retention. Traces, screenshots, and other Playwright attachments represented by the blobs remain associated with their tests in the merged result. For reporting options and investigation flow, see Playwright HTML report best practices.

5. GitLab CI parallel job example

GitLab can expand one job with the parallel keyword. CI_NODE_TOTAL contains the job count, and CI_NODE_INDEX is already 1-based. That matches Playwright's current/total format, so pass both variables directly:

stages:
  - test
  - report

playwright:
  stage: test
  image: mcr.microsoft.com/playwright:v1.60.0-noble
  parallel: 4
  variables:
    CI: "true"
  before_script:
    - npm ci
  script:
    - npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
  artifacts:
    when: always
    name: "blob-$CI_NODE_INDEX"
    paths:
      - blob-report/
    expire_in: 1 day

The image tag is illustrative and must match the Playwright package version locked by your repository. Do not copy a version tag blindly. Update the image and package together through the team's dependency process.

GitLab artifact aggregation and merge layout depend on the pipeline design. A merge job needs all blob zip files placed into one directory before running:

npx playwright merge-reports --reporter=html ./all-blob-reports

Inspect the extracted paths. If every downloaded artifact creates a nested blob-report directory, collect only the zip files into all-blob-reports through an explicit CI artifact step. Do not merge directories containing unrelated HTML or test-results output.

GitLab's parallel job index already matches Playwright's 1-based requirement. Print CI_NODE_INDEX and CI_NODE_TOTAL in the log, and still reject a current value outside 1 through total when a custom wrapper or another CI platform supplies the pair.

6. Containerized Playwright shard example

Containers make runner parity easier, but they add a version contract. The installed @playwright/test package expects compatible browser binaries. All shards should use the same image digest or tag, same lockfile, same environment, and equivalent CPU and memory.

A portable command looks like:

docker run --rm --ipc=host -e CI=true -e BASE_URL=https://qa.example.test -v "$PWD:/work" -w /work mcr.microsoft.com/playwright:v1.60.0-noble sh -lc "npm ci && npx playwright test --shard=1/4"

Repeat on four agents with 1/4 through 4/4. Replace the illustrative image tag with the version required by package-lock.json. A pinned digest gives stronger reproducibility than a mutable tag.

Do not mount one shared writable node_modules, blob-report, or test-results directory into all containers. Concurrent package writes and artifact collisions create nondeterministic failures. Each job gets private workspace output, then the CI artifact system transfers immutable blob files to the merge job.

--ipc=host is commonly recommended for Chromium in Docker on Linux to reduce shared-memory issues. Follow the security and runtime policies of your CI environment. The application URL must resolve from inside the container, so localhost refers to the container itself unless networking is configured deliberately.

When only one container fails, log the image identity, Node and Playwright versions, shard pair, worker count, and target URL. Equivalent metadata makes cross-machine comparison possible.

7. Shard-aware account fixture example

Client-side browser contexts do not prevent two machines from editing the same backend account. Allocate a unique account per worker. Playwright's parallelIndex provides a worker identity that stays within the parallel scheduling model.

import { test as base, request } from '@playwright/test';
import path from 'node:path';

type WorkerFixtures = {
  workerStorageState: string;
};

export const test = base.extend<{}, WorkerFixtures>({
  storageState: ({ workerStorageState }, use) => use(workerStorageState),

  workerStorageState: [async ({}, use) => {
    const id = base.info().parallelIndex;
    const stateFile = path.resolve(
      base.info().project.outputDir,
      '.auth',
      `worker-${id}.json`
    );
    const account = await acquireAccount(id);
    const api = await request.newContext({
      baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
      storageState: undefined,
    });

    await api.post('/test-support/login', {
      data: { email: account.email, password: account.password },
    });
    await api.storageState({ path: stateFile });
    await api.dispose();

    await use(stateFile);
    await releaseAccount(account);
  }, { scope: 'worker' }],
});

The acquireAccount and releaseAccount functions are application-specific and must coordinate atomically when several workflows can overlap. An array indexed by id is sufficient only when account ownership is guaranteed outside the test process.

The state file lives under the project output directory, which keeps worker artifacts isolated and disposable. Never commit it. Authentication state can contain reusable cookies and tokens.

If tests do not modify server-side state, one shared account may be reasonable. Make that a conscious concurrency decision. The Playwright storageState reuse examples cover shared, role-based, and worker-specific authentication patterns in more depth.

8. Build unique names and cleanup for distributed tests

CI systems can run two commits, reruns, or pull requests at the same time. A shard index alone is not globally unique. Combine a run identifier with parallelIndex and retry:

import { test, expect } from '@playwright/test';

test('creates an order without collisions', async ({ page }, testInfo) => {
  const runId = process.env.CI_RUN_ID ?? 'local';
  const orderRef = [
    'pw',
    runId,
    testInfo.parallelIndex,
    testInfo.retry,
  ].join('-');

  await page.goto('/orders/new');
  await page.getByLabel('Reference').fill(orderRef);
  await page.getByRole('button', { name: 'Create order' }).click();
  await expect(page.getByText(orderRef)).toBeVisible();
});

Use the supported API for cleanup in afterEach or a fixture teardown. Cleanup should tolerate a partially created record because a browser may fail before creation finishes. Do not hide the original test failure with a cleanup exception, but report cleanup problems separately.

Names are only one shared resource. Consider email inboxes, SMS numbers, object storage prefixes, feature flag subjects, temporary files, download folders, local ports, webhooks, and message queues. The safe identity often needs run, project, worker, and retry components.

Avoid using Date.now() alone. Parallel calls can be close, and an unlabelled timestamp is hard to trace. A structured identity helps operations teams link a database record to a CI run.

When a test intentionally validates duplicate handling, create the collision inside that test. Accidental cross-shard duplicates are infrastructure noise, not meaningful product coverage.

9. Troubleshoot playwright sharding across machines examples

An empty shard is possible when a filtered run has fewer distribution units than shards. Run the exact CI arguments with --list. Check testDir, testMatch, projects, grep, grepInvert, and fullyParallel. If file-level distribution is active and only two files match, six shards cannot all receive useful work.

Duplicated business effects do not automatically mean duplicated selection. Retries, beforeAll setup, event delivery, or non-idempotent cleanup can create repeats. Use report test IDs and shard logs to distinguish scheduling from application behavior.

For imbalance, compare duration rather than test count:

Symptom Likely cause Practical response
One shard owns one huge file File-level granularity Split the file or enable fullyParallel safely
Every shard slowed after scaling Target saturation Reduce total concurrency
One job has longer setup Runner or cache variance Standardize runner class and setup
Slow shard changes each run External latency or flaky retries Inspect traces, retries, and dependencies
Some shards are empty under grep Too many shards for filtered set Use a smaller matrix for that suite

Do not manually assign slow specs to numbered folders as the first response. Static ownership becomes stale as tests change. Improve test isolation and granularity, then let native sharding operate. If historical scheduling becomes necessary at very large scale, keep the generated test lists auditable and verify complete coverage.

Reproduce a suspicious partition with the exact commit, environment variables, project, filters, worker count, and shard pair shown in CI. A local command that changes even one filter is not the same selection. When the issue appears only on one runner, compare CPU limits, memory pressure, browser installation, DNS, proxy configuration, and deployment routing before rewriting test logic. Preserve the original blob and trace until the failure category is known.

10. Apply a production readiness checklist

Before making a sharded workflow required, deliberately fail one test and verify the entire story. The failed shard must turn the quality gate red, other shards should finish if diagnostic completeness is desired, blob artifacts should upload, the merge job should run, and the combined report should show the failure with its trace.

Then test a cancellation. Decide whether partial artifacts are retained and clearly labeled. A partial merge must never appear as a complete green suite.

Review these controls:

  • Matrix expands exactly 1/N through N/N.
  • All jobs use the same commit, package lockfile, config, projects, filters, browser versions, and BASE_URL.
  • Worker count multiplied by shard count stays within runner and environment capacity.
  • Accounts and created data cannot collide across concurrent workflows.
  • Blob artifacts have unique names and short raw retention.
  • The merged report has an appropriate longer retention and access policy.
  • Secrets are redacted from traces, logs, storage state, and uploaded environment data.
  • The slowest shard and queue time are observable.
  • Setup failures and application assertion failures are distinguishable.
  • Owners know how to reproduce one shard with its exact CLI arguments.

Revisit the checklist when adding a browser project or changing fullyParallel. Either change can alter the distribution and total load without changing the shard matrix.

Interview Questions and Answers

Q: Give a concrete four-machine sharding example.

I run the same command on four CI machines, changing only --shard to 1/4, 2/4, 3/4, and 4/4. Each job receives the same lockfile, config, projects, filters, browser version, and target URL. It emits a blob report with a unique artifact name.

Q: How do GitHub Actions and GitLab indices differ?

In a GitHub matrix I normally define 1, 2, 3, and 4 explicitly. GitLab supplies CI_NODE_INDEX from 1 through CI_NODE_TOTAL, so I pass those values directly. In both systems I log and validate the pair because Playwright expects a 1-based current value.

Q: Why not share one output directory across machines?

Concurrent writes can overwrite results, traces, screenshots, and partially written files. Each job should own a private output directory and publish immutable artifacts. The merge job is the controlled point where blob files are collected.

Q: How do you preserve reports when a shard fails?

I configure the artifact upload with an always or not-cancelled condition and use the blob reporter. The merge job depends on all shard jobs but is also allowed to run after test failures. The failed shard still makes the pipeline gate fail.

Q: What makes a worker-scoped account fixture safe?

It acquires a unique account atomically, authenticates in a clean request context, saves state under the worker's output directory, and releases the account during teardown. The identity must remain unique across overlapping workflows, not only within one process.

Q: How do you verify complete coverage after sharding?

All jobs use the same total and a unique current value from 1 to total. I verify matrix expansion and expected artifact count, and I can compare --list output during pipeline development. I avoid per-job filter differences that would make the union ambiguous.

Q: What do you do when four shards are slower than two?

I split queue, setup, execution, retry, and merge times and look for target saturation. If repeated setup or runner queues dominate, I reduce shards or improve immutable caching. If application contention dominates, I lower total workers and coordinate environment capacity.

Common Mistakes

  • Assuming a CI platform's index base without checking its official variable contract.
  • Updating the Playwright package without updating a pinned Docker image, or the reverse.
  • Varying projects or grep filters between jobs that are supposed to form one suite.
  • Letting every shard upload an artifact with the same name.
  • Sharing one writable workspace or report directory across containers.
  • Assuming browser context isolation also isolates server records and user settings.
  • Using shard number alone as a unique record ID across concurrent workflows.
  • Requiring a successful test matrix before report merging.
  • Counting tests instead of measuring the longest shard.
  • Increasing shard count when CI queue delay or environment contention is already dominant.

Conclusion

The most reusable Playwright sharding across machines examples have four parts: a correct 1-based CLI pair, an equivalent CI job, isolated resources, and a blob artifact that can be merged. GitHub Actions, GitLab CI, and container runners express the matrix differently, but those invariants do not change.

Start by running --list for two shards, then implement artifact upload and merge with one intentional failure. Only after coverage and evidence are proven should you increase the matrix and tune workers against measured slowest-shard and total feedback time.

Interview Questions and Answers

Show the CLI pattern for four Playwright shards.

Run four concurrent commands with --shard=1/4, --shard=2/4, --shard=3/4, and --shard=4/4. They must use the same revision, config, projects, filters, and target environment. Their union represents the intended suite.

How would you implement sharding in a CI matrix?

I define a fixed total and a matrix of 1-based indices. Every matrix job installs identical dependencies, runs Playwright with its pair, and uploads a uniquely named blob artifact. A dependent job downloads all blobs and merges the report.

What is the safest way to allocate test users across shards?

I allocate at worker scope using a globally unique worker identity such as testInfo.parallelIndex, backed by an atomic account pool if workflows overlap. I do not assume a browser context isolates server-side data. Accounts are released or reset after use.

Why should Docker image and Playwright package versions match?

The package drives specific browser builds and protocols. A mismatched image can lack the expected executable or dependency set. Pinning both keeps every shard behaviorally equivalent and makes failures reproducible.

How do you merge failed and passed shard results?

Shard jobs upload blob reports even after test failures. The merge job runs under a non-cancelled or always condition, downloads every available artifact, and invokes merge-reports. CI still fails the quality gate based on the failed shard.

How would you troubleshoot an empty shard?

I verify the current/total pair, project and grep filters, test discovery, and whether file-level distribution has too few eligible files. I run --list with the exact arguments and compare it with another shard. An empty shard can be valid for a tiny filtered set, but it often indicates excessive shard count.

What metrics matter for sharding efficiency?

I track queue delay, setup duration, execution duration per shard, the maximum shard duration, retries, infrastructure errors, and merge time. The maximum job plus downstream work determines feedback time. Average test count alone does not show balance.

Frequently Asked Questions

What is the simplest Playwright sharding command?

Use npx playwright test --shard=1/3 for the first of three shards, then run 2/3 and 3/3 in separate concurrent jobs. The index is 1-based and every job must use the same total.

Can I use Playwright sharding in GitLab CI?

Yes. GitLab parallel jobs expose 1-based CI_NODE_INDEX and CI_NODE_TOTAL values, which can be passed directly as --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL.

Can each Playwright shard run in Docker?

Yes. Use the same image for every shard and align the image's Playwright browser version with the package in the lockfile. Mount or upload artifacts separately and avoid sharing a writable browser profile.

Do I need a different Playwright config for every shard?

No. Use one config and pass the shard pair on the CLI. Environment-based differences should be limited to intentional values such as the target URL, credentials, and shard-aware test resource identifiers.

How can I see which tests a Playwright shard selected?

Run the same command with --list and the shard option, or use a readable reporter in a diagnostic run. Compare selections only when all jobs use identical filters and projects.

How do I stop shard artifacts from overwriting each other?

Give each CI artifact a name containing the shard index and run identifier. Keep Playwright output directories job-local, then merge only blob report files into a dedicated downstream directory.

Why did adding more shards make the pipeline slower?

Runner queues, repeated installation, browser startup, report transfer, and target-environment contention can exceed the saved execution time. Measure the entire commit-to-result path and reduce shards or cache immutable dependencies when setup dominates.

Related Guides