Parallel Runs, Sharding, and CI-Ready Playwright
Playwright tests can run in parallel across multiple workers and even multiple machines. Learn how to configure workers, shard test suites for CI, collect trace and video artifacts, and set up a.
This post is part of the Playwright Essentials series. If you missed the previous post, read Part 7 — Visual and Accessibility Checks in Playwright first.
Introduction — Speed Is a Feature
A test suite that takes 45 minutes to run is a test suite that doesn’t get run. Developers stop waiting for CI. They merge without green checks. Flaky tests get ignored because nobody has time to investigate.
Speed is not a luxury — it’s a prerequisite for adoption.
Playwright is designed for speed. Out of the box, it runs tests in parallel across multiple browser contexts. With sharding, you can distribute tests across multiple CI machines and cut total runtime by 10x or more. With trace collection, you can debug failures without reproducing them locally.
This post will show you how to configure parallel execution, how to shard test suites across CI workers, how to collect and upload test artifacts (traces, videos, screenshots), and how to build a production-grade Playwright CI pipeline in GitHub Actions.
Parallel Execution: Workers and Contexts
By default, Playwright runs tests in parallel using workers. Each worker is a separate Node.js process that runs a subset of your tests. Within each worker, tests run sequentially, but multiple workers run simultaneously.
Default Parallelism
If you have 10 tests and 5 workers, Playwright will:
- Assign ~2 tests to each worker
- Start 5 workers simultaneously
- Each worker runs its assigned tests sequentially
Result: Total runtime is roughly the time of the slowest 2 tests, not the sum of all 10.
Configuring Workers
Control the number of workers in playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
workers: 5, // Use 5 parallel workers
});
Or set workers based on available CPU cores:
export default defineConfig({
workers: process.env.CI ? 2 : undefined,
// On CI: 2 workers (CI machines are often resource-constrained)
// Locally: undefined means Playwright auto-detects based on CPU cores
});
:::tip[Start Conservative on CI] CI runners often have limited CPU and memory. Start with 2–4 workers and increase based on observed stability. Too many workers can cause resource contention and increase flakiness. :::
Fully Parallel Mode
By default, tests in the same file run sequentially within a worker. If you want tests in the same file to run in parallel, enable fullyParallel:
export default defineConfig({
fullyParallel: true,
});
Or per-file:
// tests/checkout.spec.ts
import { test } from '@playwright/test';
test.describe.configure({ mode: 'parallel' });
test('checkout with credit card', async ({ page }) => { /* ... */ });
test('checkout with PayPal', async ({ page }) => { /* ... */ });
:::warning[Shared State and Fully Parallel]
If tests in a file modify shared state (database records, account settings), enabling fullyParallel may cause race conditions. Use separate test accounts or disable parallel mode for those tests.
:::
Serial Mode for Order-Dependent Tests
If tests must run in a specific order (e.g., “create user” → “edit user” → “delete user”), use .serial():
test.describe.serial('user lifecycle', () => {
test('create user', async ({ page }) => { /* ... */ });
test('edit user', async ({ page }) => { /* ... */ });
test('delete user', async ({ page }) => { /* ... */ });
});
These tests will run sequentially within their describe block, but other tests can still run in parallel.
Sharding: Distributing Tests Across Machines
Sharding splits your test suite into N parts and runs each part on a separate machine. If you have 100 tests and 4 CI machines, each machine runs 25 tests. Total runtime drops to ~25% of the serial time.
How Sharding Works
Playwright’s --shard flag takes a ratio: --shard=X/N means “I am machine X of N total machines.”
Example with 4 shards:
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4
Each command runs a different 25% of the test suite. Run all four in parallel (on different machines or in different CI jobs), and you’ve quartered the total runtime.
Sharding in GitHub Actions
Here’s a production-ready GitHub Actions workflow with sharding:
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run Playwright tests
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-results-${{ matrix.shardIndex }}
path: test-results/
retention-days: 7
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ matrix.shardIndex }}
path: playwright-report/
retention-days: 7
This workflow:
- Creates 4 parallel jobs (one per shard)
- Installs dependencies and Playwright browsers
- Runs 1/4 of the test suite on each job
- Uploads test results and HTML reports as artifacts
If you have 200 tests that take 40 minutes serially, sharding across 4 machines reduces CI time to ~10 minutes.
Adaptive Sharding
If your test suite is small (< 50 tests), sharding may not help — the overhead of starting 4 CI jobs may exceed the parallelism benefit. Use dynamic sharding based on suite size:
strategy:
matrix:
shardIndex: ${{ github.event_name == 'pull_request' && fromJSON('[1, 2]') || fromJSON('[1, 2, 3, 4]') }}
shardTotal: ${{ github.event_name == 'pull_request' && 2 || 4 }}
This runs 2 shards on PRs (faster feedback) and 4 shards on main branch (deeper coverage).
Artifacts: Traces, Videos, and Screenshots
When tests fail in CI, you need diagnostic data. Playwright captures traces, videos, and screenshots automatically.
Traces
A trace is a complete recording of the test execution: every action, every network request, every DOM snapshot. You can open it in Playwright’s trace viewer and step through the test as if you were debugging it live.
Configure trace collection in playwright.config.ts:
export default defineConfig({
use: {
trace: 'on-first-retry',
// Options: 'off', 'on', 'retain-on-failure', 'on-first-retry'
},
});
Recommended settings:
- Locally:
'off'(traces are large and slow) - CI:
'on-first-retry'(only capture traces for flaky or failing tests)
Videos
Videos show exactly what the browser did during the test. They’re easier to review than traces but less detailed.
export default defineConfig({
use: {
video: 'retain-on-failure',
// Options: 'off', 'on', 'retain-on-failure', 'on-first-retry'
},
});
Screenshots
Playwright captures screenshots on failure automatically. You can also take screenshots manually:
test('homepage loads', async ({ page }) => {
await page.goto('/');
await page.screenshot({ path: 'homepage.png' });
});
Uploading Artifacts in CI
Artifacts are useless if they stay on the CI machine. Upload them so developers can download and review:
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-artifacts-${{ matrix.shardIndex }}
path: |
test-results/
playwright-report/
retention-days: 7
When a test fails, go to the GitHub Actions run, download the artifact, and open the trace:
npx playwright show-trace trace.zip
The trace viewer opens in your browser with a full timeline of the test execution.
Retries: Handling Flaky Tests
Real-world applications are flaky. Network requests time out. APIs return 500 errors transiently. Animations interfere with clicks. Retrying failed tests reduces false negatives.
Configure retries in playwright.config.ts:
export default defineConfig({
retries: process.env.CI ? 2 : 0,
// On CI: retry failed tests up to 2 times
// Locally: don't retry (fail fast so you fix the issue)
});
When a test fails, Playwright:
- Runs the test again (first retry)
- If it still fails, runs it again (second retry)
- If it still fails, marks the test as failed
If any retry succeeds, the test is marked as flaky (passed with retries). Flaky tests pass the CI build but are flagged in the report so you can investigate.
:::warning[Retries Mask Problems] Retries reduce flakiness but hide root causes. Track flaky test counts in your CI metrics. If a test is flaky more than 10% of the time, fix the test or the application, don’t just retry indefinitely. :::
Playwright Reporter for CI
Playwright’s built-in HTML reporter is great for local development but too heavy for CI logs. Use the list reporter for CI:
export default defineConfig({
reporter: process.env.CI
? [['list'], ['html', { open: 'never' }]]
: [['html', { open: 'on-failure' }]],
});
This configuration:
- On CI: Outputs a line-by-line list of test results to stdout, generates an HTML report but doesn’t open it
- Locally: Opens the HTML report automatically on failure
You can also integrate with third-party reporters:
- GitHub Actions:
@playwright/test/reporteroutputs annotations on the PR - JUnit XML:
['junit', { outputFile: 'test-results/junit.xml' }]for integration with test management tools - Allure:
allure-playwrightfor rich historical reporting
Environment Variables and Secrets
Tests often need API keys, database credentials, or OAuth tokens. Never commit secrets to source control.
Using GitHub Secrets
Store secrets in GitHub repository settings:
- Go to Settings → Secrets and variables → Actions
- Add secrets (e.g.,
TEST_USER_EMAIL,TEST_USER_PASSWORD,API_KEY) - Reference them in the workflow:
- name: Run Playwright tests
env:
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
API_KEY: ${{ secrets.API_KEY }}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
In your tests, read from process.env:
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', process.env.TEST_USER_EMAIL!);
await page.fill('input[name="password"]', process.env.TEST_USER_PASSWORD!);
await page.click('button[type="submit"]');
});
:::tip[Fail Fast on Missing Secrets] Add a validation step at the start of your test suite:
// global-setup.ts
if (!process.env.TEST_USER_EMAIL || !process.env.TEST_USER_PASSWORD) {
throw new Error('Missing required environment variables: TEST_USER_EMAIL, TEST_USER_PASSWORD');
}
This fails the build immediately with a clear message, rather than 50 cryptic test failures. :::
Complete GitHub Actions Workflow
Here’s a production-ready Playwright CI pipeline:
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
workflow_dispatch:
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run Playwright tests
env:
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-results-${{ matrix.shardIndex }}
path: test-results/
retention-days: 7
- name: Upload Playwright HTML report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ matrix.shardIndex }}
path: playwright-report/
retention-days: 7
merge-reports:
if: always()
needs: test
runs-on: ubuntu-latest
steps:
- name: Download all reports
uses: actions/download-artifact@v4
with:
path: all-reports
- name: Merge reports
run: npx playwright merge-reports ./all-reports
- name: Upload merged report
uses: actions/upload-artifact@v4
with:
name: playwright-report-merged
path: playwright-report/
retention-days: 30
This workflow:
- Runs tests in 4 parallel shards
- Uploads individual shard reports
- Merges all shard reports into a single HTML report
- Uploads the merged report
Developers can download the merged report and view all test results in one place.
Common CI Pitfalls
Pitfall 1: Not Installing Browser Dependencies
Playwright browsers need system dependencies (fonts, libraries). If you see errors like “Could not find browser binary,” install them:
npx playwright install --with-deps chromium
Pitfall 2: Running All Browsers in CI
Don’t run Chromium, Firefox, and WebKit in every CI run. It triples runtime for minimal value. Run Chromium by default:
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
});
Run cross-browser tests on a schedule (nightly) or manually before releases.
Pitfall 3: Ignoring Flaky Tests
If you configure retries: 3 and ignore flaky test warnings, you’ve built a CI system that hides problems. Set a flaky test budget (e.g., “no more than 2% flaky rate”) and investigate when you exceed it.
Conclusion
Playwright is designed for speed and scale. Parallel execution, sharding, and trace collection turn a slow, unreliable test suite into a fast, debuggable CI pipeline.
Key takeaways:
- Use workers to run tests in parallel locally
- Use sharding to distribute tests across multiple CI machines
- Collect traces and videos only on failure to minimize storage cost
- Configure retries on CI to handle transient flakiness
- Upload artifacts so developers can debug failures without reproducing locally
- Use GitHub Secrets for credentials, never commit them to source control
:::tip[Measure and Iterate] Track your CI metrics: total runtime, flaky test rate, pass rate, artifact size. Set targets (e.g., “all tests run in under 10 minutes”) and optimize your configuration to hit them. :::
Action for this week: Add sharding to your Playwright CI pipeline. Measure the before/after runtime. If your suite is small (< 50 tests), skip sharding but configure trace collection on failure. Download a trace from a failed test and explore the trace viewer — you’ll see why it’s the best debugging tool for E2E tests.
Next in this series: Part 9 — Debugging Failed Playwright Tests Like a Pro