# Parallel Execution and Sharding > **When to use**: Speeding up test suites by running tests concurrently within a single machine (parallelism) or across multiple machines (sharding). Essential once your suite exceeds 5 minutes. ## Quick Reference ```bash # Workers (parallelism within one machine) npx playwright test --workers=4 # fixed worker count npx playwright test --workers=50% # percentage of CPU cores # Sharding (splitting across machines) npx playwright test --shard=1/4 # run first quarter npx playwright test --shard=2/4 # run second quarter # Merging shard results npx playwright merge-reports ./blob-report # merge to default HTML npx playwright merge-reports --reporter=html,json ./blob-report # multiple formats # Fully parallel mode npx playwright test --fully-parallel # override config for this run ``` ## Patterns ### Pattern 1: Configuring Workers **Use when**: Controlling how many tests run simultaneously on one machine. **Avoid when**: You only have 1-2 tests (parallelism has no effect). ```ts // playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ // fullyParallel: true means tests WITHIN a file also run in parallel. // Without it, only files run in parallel (tests within a file are serial). fullyParallel: true, // Workers: how many parallel processes // - undefined: auto-detect (half CPU cores, capped at a reasonable number) // - number: fixed count // - string percentage: '50%' of CPU cores workers: process.env.CI ? "50%" : undefined, }) ``` **What `fullyParallel` actually controls:** | Setting | Files run in parallel | Tests within a file run in parallel | | -------------------------------- | --------------------- | ----------------------------------- | | `fullyParallel: false` (default) | Yes | No -- serial within each file | | `fullyParallel: true` | Yes | Yes -- every test is independent | **Per-file override when one file needs serial execution:** ```ts // tests/onboarding.spec.ts import { test, expect } from "@playwright/test" // This file's tests run serially even with fullyParallel: true in config test.describe.configure({ mode: "serial" }) test("step 1: enter company name", async ({ page }) => { // ... }) test("step 2: choose plan", async ({ page }) => { // ... }) ``` ### Pattern 2: Sharding Across CI Machines **Use when**: Suite is too slow for a single machine even with maximum workers. Split work across N separate CI jobs. **Avoid when**: Suite runs under 5 minutes on one machine. Sharding splits the test file list into N equal groups. Each shard runs one group. ```bash # Machine 1 Machine 2 Machine 3 Machine 4 --shard=1/4 --shard=2/4 --shard=3/4 --shard=4/4 ``` **Config for sharded runs -- use blob reporter:** ```ts // playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ fullyParallel: true, workers: process.env.CI ? "50%" : undefined, // Blob reporter outputs a binary file that can be merged later. // In non-CI, use HTML for local viewing. reporter: process.env.CI ? [["blob"], ["github"]] : [["html", { open: "on-failure" }]], }) ``` ### Pattern 3: Merging Blob Reports from Shards **Use when**: You sharded your tests and need a single unified report. **Avoid when**: No sharding -- the regular HTML reporter works directly. Each shard produces a `.zip` file in `blob-report/`. After all shards complete, merge them: ```bash # Download all blob-report/ directories into one folder, then: npx playwright merge-reports --reporter=html ./all-blob-reports # Multiple output formats npx playwright merge-reports --reporter=html,json,junit ./all-blob-reports # Custom output directory PLAYWRIGHT_HTML_REPORT=merged-report npx playwright merge-reports --reporter=html ./all-blob-reports ``` **GitHub Actions example (merge job):** ```yaml merge-reports: if: ${{ !cancelled() }} needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - run: npm ci - uses: actions/download-artifact@v4 with: path: all-blob-reports pattern: blob-report-* merge-multiple: true - run: npx playwright merge-reports --reporter=html ./all-blob-reports - uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 14 ``` ### Pattern 4: Worker-Scoped Fixtures for Shared Resources **Use when**: Parallel workers each need an expensive resource (database connection, auth token) that should be created once per worker, not once per test. **Avoid when**: The resource is cheap to create -- use a regular test-scoped fixture. ```ts // fixtures.ts import { test as base } from "@playwright/test" type WorkerFixtures = { dbConnection: DatabaseClient workerAuthToken: string } export const test = base.extend<{}, WorkerFixtures>({ // Created once per worker process, shared across all tests in that worker dbConnection: [ async ({}, use) => { const db = await DatabaseClient.connect(process.env.DB_URL!) await use(db) await db.disconnect() }, { scope: "worker" }, ], workerAuthToken: [ async ({}, use, workerInfo) => { // Each worker gets a unique user to avoid test interference const response = await fetch(`${process.env.API_URL}/auth/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: `worker-user-${workerInfo.workerIndex}`, password: process.env.TEST_PASSWORD, }), }) const { token } = await response.json() await use(token) }, { scope: "worker" }, ], }) export { expect } from "@playwright/test" ``` ### Pattern 5: Test Isolation for Safe Parallelism **Use when**: Preparing tests to run in parallel without interference. **Avoid when**: Never -- isolation is always required for reliable parallel execution. **The golden rule**: Each test must create its own state and clean up after itself. No test should depend on or modify state that another test uses. ```ts // BAD: Tests share a hardcoded user -- parallel runs collide test("update profile", async ({ page }) => { await page.goto("/users/shared-user/profile") await page.getByLabel("Name").fill("New Name") await page.getByRole("button", { name: "Save" }).click() // Another parallel test also editing "shared-user" -- race condition! }) // GOOD: Each test creates its own user test("update profile", async ({ page, request }) => { // Create a unique user for this test const res = await request.post("/api/test/users", { data: { name: `user-${Date.now()}`, email: `${Date.now()}@test.com` }, }) const user = await res.json() await page.goto(`/users/${user.id}/profile`) await page.getByLabel("Name").fill("Updated Name") await page.getByRole("button", { name: "Save" }).click() await expect(page.getByLabel("Name")).toHaveValue("Updated Name") // Cleanup await request.delete(`/api/test/users/${user.id}`) }) ``` **Using `workerInfo` and `testInfo` for unique identifiers:** ```ts import { test, expect } from "@playwright/test" test("create order", async ({ page }, testInfo) => { const uniqueId = `order-${testInfo.workerIndex}-${Date.now()}` // Use uniqueId for any data this test creates await page.goto(`/orders/new?ref=${uniqueId}`) // ... }) ``` ### Pattern 6: Dynamic Shard Count Based on Test Count **Use when**: You want to automatically adjust shard count based on how many tests exist, rather than hardcoding. **Avoid when**: Your test count is stable and a fixed shard count works well. ```yaml # .github/workflows/playwright.yml -- dynamic shard calculation jobs: determine-shards: runs-on: ubuntu-latest outputs: shard-count: ${{ steps.calc.outputs.count }} shard-matrix: ${{ steps.calc.outputs.matrix }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - run: npm ci - id: calc run: | TEST_COUNT=$(npx playwright test --list --reporter=json 2>/dev/null | node -e " const data = require('fs').readFileSync('/dev/stdin', 'utf8'); const parsed = JSON.parse(data); console.log(parsed.suites?.reduce((acc, s) => acc + (s.specs?.length || 0), 0) || 0); ") # 1 shard per 20 tests, minimum 1, maximum 8 SHARDS=$(( (TEST_COUNT + 19) / 20 )) SHARDS=$(( SHARDS > 8 ? 8 : SHARDS )) SHARDS=$(( SHARDS < 1 ? 1 : SHARDS )) # Build matrix array: ["1/N", "2/N", ...] MATRIX="[" for i in $(seq 1 $SHARDS); do [ $i -gt 1 ] && MATRIX+="," MATRIX+="\"$i/$SHARDS\"" done MATRIX+="]" echo "count=$SHARDS" >> $GITHUB_OUTPUT echo "matrix=$MATRIX" >> $GITHUB_OUTPUT test: needs: determine-shards runs-on: ubuntu-latest strategy: fail-fast: false matrix: shard: ${{ fromJson(needs.determine-shards.outputs.shard-matrix) }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - run: npm ci - run: npx playwright install --with-deps - run: npx playwright test --shard=${{ matrix.shard }} ``` ## Decision Guide | Scenario | Workers | Shards | Why | | -------------------------------------- | ---------------------------------- | ------------ | ------------------------------------------- | | < 50 tests, < 5 min | Auto (default) | None | No optimization needed | | 50-200 tests, 5-15 min | `'50%'` in CI | 2-4 shards | Balance speed and CI cost | | 200+ tests, > 15 min | `'50%'` in CI | 4-8 shards | Keep feedback under 10 min | | Flaky tests due to resource contention | Reduce: `workers: 2` | Keep current | Fewer workers = less CPU/memory pressure | | Tests modify shared database | `workers: 1` or isolate per worker | Still useful | Sharding splits files; workers run them | | CI has limited CPU/RAM | `workers: 1` or `'25%'` | More shards | Compensate fewer workers with more machines | | Question | `workers` (in-process) | `--shard` (across machines) | | ---------------------- | ----------------------------------------- | ---------------------------------- | | What does it split? | Tests across CPU cores on one machine | Test files across separate CI jobs | | Controlled by? | `playwright.config.ts` or `--workers` CLI | `--shard=X/Y` CLI flag | | Shares memory? | Yes (same machine) | No (separate machines) | | Report merging needed? | No (single process) | Yes (`merge-reports`) | | Cost | Free (same machine) | More CI minutes (more machines) | ## Anti-Patterns | Anti-Pattern | Problem | Do This Instead | | ----------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------- | | `fullyParallel: false` with no reason | Tests within files run serially; slow suite | Set `fullyParallel: true` unless specific tests need serial | | `workers: 1` in CI "to be safe" | Negates parallelism entirely | Fix isolation issues; use `workers: '50%'` | | Tests sharing a hardcoded user account | Race conditions when parallel -- both tests modify same data | Each test creates unique data via API or fixture | | `--shard=1/4` without blob reporter | Each shard produces its own HTML report; no merged view | Configure `reporter: [['blob']]` for sharded CI runs | | Sharding with 3 tests | Overhead of shard setup exceeds time saved | Only shard when suite exceeds 5 minutes | | `test.describe.serial()` everywhere | Kills parallelism, creates hidden dependencies | Use only when tests genuinely depend on prior state | | Worker count higher than CPU cores | Context switching overhead; slower, not faster | Use `'50%'` or let Playwright auto-detect | | Not using `fail-fast: false` in CI matrix | One shard failure cancels others; incomplete results | Always set `fail-fast: false` for sharded strategies | ## Troubleshooting ### Tests pass alone but fail when run together **Cause**: Shared state between tests -- database rows, cookies, global variables, file system. **Fix**: Isolate each test. Use unique data per test: ```ts test("create order", async ({ page, request }, testInfo) => { // Unique product per test -- no collision with parallel tests const product = await request.post("/api/test/products", { data: { name: `Widget-${testInfo.workerIndex}-${Date.now()}` }, }) // ... }) ``` ### Shard produces no tests: "No tests found" **Cause**: Shard count exceeds the number of test files. A shard gets zero files. **Fix**: Reduce shard count to at most the number of test files: ```bash # If you have 10 test files, max 10 shards npx playwright test --shard=1/10 # OK npx playwright test --shard=1/20 # Some shards will be empty ``` ### Merged report missing some test results **Cause**: Blob report files from a shard were not downloaded or were overwritten due to name collision. **Fix**: Give each shard's artifact a unique name: ```yaml # Each shard - uses: actions/upload-artifact@v4 with: name: blob-report-${{ strategy.job-index }} # unique per shard path: blob-report/ # Merge step - uses: actions/download-artifact@v4 with: pattern: blob-report-* merge-multiple: true path: all-blob-reports ``` ### Worker-scoped fixture not shared -- recreated per test **Cause**: Missing `{ scope: 'worker' }` option, or the fixture depends on a test-scoped fixture. **Fix**: Ensure the fixture uses worker scope and only depends on worker-scoped fixtures: ```ts export const test = base.extend<{}, { sharedResource: Resource }>({ sharedResource: [ async ({}, use) => { const resource = await Resource.create() await use(resource) await resource.destroy() }, { scope: "worker" }, ], // Don't forget this }) ``` ### Tests are slower with more workers **Cause**: Machine is CPU- or memory-bound. More workers cause thrashing. **Fix**: Reduce workers until you find the sweet spot: ```ts // playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ workers: process.env.CI ? 2 : undefined, // Start low, increase if stable }) ``` ## Related - [ci/ci-github-actions.md](ci-github-actions.md) -- sharded GitHub Actions workflow - [ci/ci-gitlab.md](ci-gitlab.md) -- GitLab `parallel:` keyword with sharding - [ci/ci-other.md](ci-other.md) -- sharding on CircleCI, Azure DevOps, Jenkins - [ci/reporting-and-artifacts.md](reporting-and-artifacts.md) -- blob reporter and merge-reports - [core/fixtures-and-hooks.md](../core/fixtures-and-hooks.md) -- worker-scoped fixtures - [core/test-organization.md](../core/test-organization.md) -- parallel vs serial execution