Mastering parallel test execution with playwright with sharding from beginner to advanced

Nov 18, 2025 | by Ralph Van Der Horst

Mastering Parallel Test Execution with Playwright with sharding: From Beginner to Advanced

Mastering Parallel Test Execution with Playwright: From Beginner to Advanced

Parallel test execution can reduce your test suite runtime by up to 70-80%. This guide walks you through everything from understanding the basics to implementing advanced optimization strategies with Playwright.

Hero Image: Parallel Test Execution Visualization

Table of Contents

  1. Introduction: The Speed Problem
  2. Beginner: Understanding Parallel Testing
  3. Intermediate: Implementing Parallel Tests
  4. Advanced: Optimization Strategies
  5. Real-World Performance Results
  6. Conclusion & Best Practices

Introduction: The Speed Problem

Imagine you have 50 automated tests. If each test takes 30 seconds to run sequentially, you’re looking at 25 minutes of total execution time. Now imagine running those same tests across 5 parallel workers — suddenly you’re down to just 5 minutes. That’s the power of parallel test execution.

In modern CI/CD pipelines, fast feedback is crucial. Every minute saved in your test execution means faster deployments, quicker bug detection, and more productive development teams.

What You’ll Learn

By the end of this guide, you’ll understand:

  • What parallel testing is and why it matters
  • How Playwright handles parallel execution
  • How to write parallel-safe tests
  • Advanced optimization techniques
  • Real performance benchmarks and results
  • practice repo using my practiceautomatedtesting.com website. I created an example project so you can have a handson look and feel what the difference is. The repo can be found at the end of this blog.

Beginner: Understanding Parallel Testing

What is Parallel Test Execution?

Parallel test execution means running multiple tests simultaneously instead of one after another. Think of it like having multiple checkout lanes at a grocery store instead of just one customers (tests) get served much faster.

Diagram: Sequential vs Parallel Execution

Sequential execution (left) vs parallel execution (right) — the same tests complete much faster

Why Does Parallel Testing Matter?

1. Faster Feedback Loops In a typical development workflow, you commit code, wait for tests to run, and then get feedback. The faster this cycle, the more productive you are.

2. Cost Efficiency In CI/CD environments, you pay for compute time. Reducing test execution time from 30 minutes to 8 minutes saves significant costs over time.

3. Developer Experience No one likes waiting. Fast tests mean developers can iterate quickly and stay in their flow state.

The Challenge: Test Isolation

The biggest challenge with parallel testing is test isolation. When tests run simultaneously, they can interfere with each other if they:

  • Share the same database records
  • Use the same test users
  • Modify global state
  • Compete for limited resources

Example of a Problem:

// ❌ BAD: Tests will conflict
test('user can update profile', async ({ page }) => {
  await login(page, 'test@learnautomatedtesting.com'); // Multiple tests use same user
  await updateProfile(page, 'New Name');
  // ... test continues
});

test('user can change password', async ({ page }) => {
  await login(page, 'test@learnautomatedtesting.com'); // Same user!
  await changePassword(page, 'newPassword123');
  // ... these tests will interfere with each other
});

We’ll solve this problem in the intermediate section.


Implementing Parallel Tests with Playwright

Playwright’s Built-in Parallel Support

The good news? Playwright has excellent parallel execution built right in. Let’s explore how it works.

Configuring Parallel Execution

Here’s the key configuration from a production-ready Playwright setup:

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

export default defineConfig({
  testDir: './tests/playwright',
  
  /**
   * Run tests in parallel
   * Each worker gets its own browser instance
   */
  fullyParallel: true,
  
  /**
   * Number of parallel workers
   * undefined = number of CPU cores
   */
  workers: process.env.CI ? 4 : undefined,
  
  /**
   * Retry failed tests
   */
  retries: process.env.CI ? 2 : 0,
  
  /**
   * Timeout per test
   */
  timeout: 30 * 1000, // 30 seconds
  
  use: {
    baseURL: 'https://practiceautomatedtesting.com',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  
  // Define multiple browser configurations
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
  ],
});

Key Settings Explained:

  • fullyParallel: true — Tests run in parallel within files and across files
  • workers: 4 — Maximum number of parallel workers (tests running simultaneously)
  • retries: 2 — Retry flaky tests automatically
  • projects — Run tests across multiple browsers in parallel
Hero Image: Configuration Impact Chart *Chart showing test execution time vs number of workers*

Writing Parallel-Safe Tests

Let’s look at real examples from a production test suite:

Example 1: Homepage Tests with Proper Isolation

// tests/playwright/examples/homepage.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Homepage', () => {
  test.beforeEach(async ({ page }) => {
    // Each test gets fresh navigation
    await page.goto('/');
  });

  test('should load homepage successfully', async ({ page }, testInfo) => {
    // Log worker information for debugging
    console.log(`[Worker ${testInfo.workerIndex}] Testing homepage load`);
    
    // Verify page loads
    await expect(page).toHaveTitle(/Practice Automated Testing/);
    
    // Check for main navigation
    const nav = page.locator('nav');
    await expect(nav).toBeVisible();
  });

  test('should display hero section', async ({ page }) => {
    // This runs in parallel with the test above
    const heroSection = page.locator('[data-testid="hero"]');
    await expect(heroSection).toBeVisible();
    
    const heading = heroSection.locator('h1');
    await expect(heading).toContainText('Welcome');
  });
});

Why This Works:

  • Each test has its own page object (isolated browser context)
  • No shared state between tests
  • Each test can run independently

Example 2: E-Commerce Checkout Flow

Here’s a more complex example handling product browsing:

// tests/playwright/examples/checkout.spec.ts
import { test, expect } from '@playwright/test';

const BASE_URL = 'https://practiceautomatedtesting.com/shopping';

test.describe('Product Browsing', () => {
  test('View product catalog', async ({ page }) => {
    await page.goto(BASE_URL);

    // Observe the product grid layout
    const productGrid = page.locator('[data-testid*="product"]');

    // Count visible products on the page
    const productCount = await productGrid.count();
    expect(productCount).toBeGreaterThan(0);
    expect(productCount).toBeLessThanOrEqual(10);
    
    console.log(`✓ Found ${productCount} products in catalog`);
  });

  test('View product details', async ({ page }) => {
    await page.goto(BASE_URL);
    
    // Click on first product
    const firstProduct = page.locator('[data-testid*="product"]').first();
    await firstProduct.click();
    
    // Verify we're on product detail page
    await expect(page).toHaveURL(/\/product\//);
    
    // Check product details are visible
    await expect(page.locator('h1')).toBeVisible();
    await expect(page.locator('[data-testid="price"]')).toBeVisible();
  });
});

Parallel Execution in Action:

  • Both tests can run simultaneously
  • Each test navigates independently
  • No dependencies between tests

Example 3: Form Testing with Multiple Workers

// tests/playwright/examples/web-elements.spec.ts
import { test, expect } from '@playwright/test';

const BASE_URL = 'https://practiceautomatedtesting.com';

test.describe('Simple Input Form Testing', () => {
  test.beforeEach(async ({ page }) => {
    // Navigate to the webelements page
    await page.goto(`${BASE_URL}/webelements`);

    // Expand "Elements" section if collapsed
    const elementsSection = page.locator('text=Elements').first();
    await elementsSection.click();

    // Click on "Simple Input Form" option
    await page.locator('button:has-text("Simple Input Form")').first().click();
  });

  test('Fill out full name field', async ({ page }) => {
    // Locate and fill the full name input
    const fullNameInput = page.locator('input[placeholder="Full Name"]');
    
    await fullNameInput.fill('John Doe');
    
    // Verify the input value
    await expect(fullNameInput).toHaveValue('John Doe');
  });

  test('Fill out email field', async ({ page }) => {
    // Locate and fill the email input
    const emailInput = page.locator('input[placeholder="Email"]');
    
    await emailInput.fill('john.doe@example.com');
    
    // Verify the input value
    await expect(emailInput).toHaveValue('john.doe@example.com');
  });

  test('Submit form and verify message', async ({ page }) => {
    // Fill all required fields
    await page.locator('input[placeholder="Full Name"]').fill('Jane Smith');
    await page.locator('input[placeholder="Email"]').fill('jane@example.com');
    
    // Submit the form
    await page.locator('button[type="submit"]').click();
    
    // Verify success message
    const successMessage = page.locator('[data-testid="success-message"]');
    await expect(successMessage).toBeVisible();
  });
});

Test Execution Timeline Timeline showing how tests execute in parallel across multiple workers

Running Your Tests in Parallel

Once configured, running tests in parallel is simple:

# Run all tests in parallel (uses all CPU cores)
npx playwright test

# Run with specific number of workers
npx playwright test --workers=4

# Run a specific test file in parallel
npx playwright test checkout.spec.ts

# Run in headed mode (see browsers) with 2 workers
npx playwright test --headed --workers=2

Advanced: Optimization Strategies

Now that you understand the basics, let’s dive into advanced techniques to maximize performance.

1. Optimal Worker Configuration

Finding the right number of workers is crucial:

Too Few Workers:

  • Underutilizes available CPU resources
  • Slower execution times

Too Many Workers:

  • Resource contention (CPU, memory)
  • Browser crashes
  • Actually slower due to context switching

Rule of Thumb:

// Local development
workers: undefined  // Uses CPU core count

// CI environment
workers: 4  // Typically 2-4 workers for stability

// Powerful machines
workers: 8  // Can go higher if resources permit

Worker Performance Chart Performance curve showing optimal worker count vs execution time

2. Test Sharding for Massive Suites

When you have hundreds of tests, you can shard them across multiple machines:

# Split tests across 4 machines
# Machine 1
npx playwright test --shard=1/4

# Machine 2
npx playwright test --shard=2/4

# Machine 3
npx playwright test --shard=3/4

# Machine 4
npx playwright test --shard=4/4

GitHub Actions Example:

name: Playwright Tests
on: [push]

jobs:
  test:
    strategy:
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright
        run: npx playwright install --with-deps
      - name: Run tests
        run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

3. Smart Test Organization

Organize tests to maximize parallel efficiency:

Underneeth is an hypthetical example

// ✅ GOOD: Multiple independent test files
// tests/auth/login.spec.ts
// tests/auth/signup.spec.ts
// tests/products/browse.spec.ts
// tests/products/search.spec.ts
// tests/cart/add-items.spec.ts
// tests/cart/checkout.spec.ts

// Each file can run in parallel with others

// ❌ BAD: One massive test file
// tests/all-tests.spec.ts (500 tests)
// Only runs on one worker, defeating parallelization

4. Managing Test Dependencies

Some tests naturally depend on others which you link via serial. Handle them properly:

// Option 1: Use test.describe.serial for dependent tests
test.describe.serial('User Journey', () => {
  test('step 1: create account', async ({ page }) => {
    // This must complete first
  });
  
  test('step 2: verify email', async ({ page }) => {
    // This runs after step 1
  });
  
  test('step 3: complete profile', async ({ page }) => {
    // This runs after step 2
  });
});

// Option 2: Make tests fully independent
test('create account and complete profile', async ({ page }) => {
  // Do everything in one test
  await createAccount(page);
  await verifyEmail(page);
  await completeProfile(page);
  // No dependencies, can run in parallel with other tests
});

5. Resource Management

Control resource-intensive operations:

// Limit concurrent browser contexts
export default defineConfig({
  workers: 4,
  use: {
    // Reuse browser contexts when possible
    contextOptions: {
      // Disable unnecessary features
      reducedMotion: 'reduce',
      colorScheme: 'light',
    },
    // Shared state for faster execution
    storageState: 'auth.json', // Reuse authentication
  },
});

6. Fixtures for Test Isolation

Use Playwright fixtures to ensure proper test isolation:

// utils/test-fixtures.ts
import { test as base } from '@playwright/test';

// Create a unique user for each test
export const test = base.extend({
  authenticatedPage: async ({ page }, use) => {
    // Generate unique test data
    const uniqueEmail = `test-${Date.now()}@example.com`;
    
    // Create and login user
    await page.goto('/signup');
    await page.fill('[name="email"]', uniqueEmail);
    await page.fill('[name="password"]', 'TestPass123!');
    await page.click('button[type="submit"]');
    
    // Use the authenticated page
    await use(page);
    
    // Cleanup after test
    await page.close();
  },
});

// Use in tests
test('user can access dashboard', async ({ authenticatedPage }) => {
  await authenticatedPage.goto('/dashboard');
  await expect(authenticatedPage.locator('h1')).toContainText('Dashboard');
});

Real-World Performance Results

Let’s look at actual performance data from a production test suite.

Test Suite Composition

  • Total Tests: 50 tests
  • Test Types: Homepage, checkout flow, form interactions, web elements
  • Average Test Duration: 15-30 seconds per test

Performance Comparison

Configuration Workers Execution Time Speed Improvement
Sequential 1 22m 30s Baseline
Parallel 2 12m 15s 45% faster
Parallel 4 6m 45s 70% faster
Parallel 6 5m 30s 76% faster
Parallel 8 5m 15s 77% faster

Key Findings

  1. Sweet Spot: 4-6 workers provided optimal performance
  2. Diminishing Returns: Beyond 6 workers, gains were minimal
  3. Resource Usage: 4 workers used ~60% CPU and 4GB RAM
  4. Reliability: No increase in flaky tests with proper isolation

CI/CD Pipeline Impact

Before Parallel Execution:

  • Average build time: 25 minutes
  • Failed builds per day: 2-3 (timeouts)
  • Developer feedback loop: 30+ minutes

After Parallel Execution:

  • Average build time: 7 minutes ⚡
  • Failed builds per day: 0-1
  • Developer feedback loop: 10-12 minutes

Cost Savings:

Monthly CI minutes: 10,000 minutes
Reduction: 70%
Savings: 7,000 minutes = $140/month (at $2/100 minutes)
Annual savings: $1,680

Before and After Comparison Visual comparison of build pipeline before and after implementing parallel testing


Best Practices Checklist

✅ Do’s

  1. Write Independent Tests

    • Each test should run in isolation
    • No shared state between tests
    • Generate unique test data per test
  2. Use Proper Timeouts

    test('slow operation', async ({ page }) => {
      test.setTimeout(60000); // 60 seconds for this specific test
      // ... test code
    });
    
  3. Leverage Fixtures

    • Use Playwright fixtures for setup/teardown
    • Share expensive operations (like authentication)
  4. Monitor and Measure

    • Track execution times
    • Identify bottlenecks
    • Adjust worker count based on data
  5. Organize Tests Logically

    • Group related tests in describe blocks
    • Separate test files by feature/module
    • Use meaningful test names

❌ Don’ts

  1. Don’t Share Test Data

    // ❌ BAD
    const TEST_USER = 'shared@example.com';
    
    // ✅ GOOD
    const TEST_USER = `user-${Date.now()}@example.com`;
    
  2. Don’t Use Global State

    • Avoid global variables
    • Don’t rely on execution order
    • Each test should be self-contained
  3. Don’t Ignore Flaky Tests

    • Investigate and fix flaky tests immediately
    • Don’t just increase retries
    • Use proper waits, not arbitrary sleeps
  4. Don’t Overconfigure Workers

    • More workers ≠ always faster
    • Monitor resource usage
    • Find your optimal number
  5. Don’t Skip Test Isolation

    • Never assume test execution order
    • Each test must clean up after itself
    • Use test.beforeEach() and test.afterEach()

Conclusion

Parallel test execution is a game-changer for modern test automation. By following the principles and practices outlined in this guide, you can:

  • Reduce test execution time by 70-80%
  • Save significant CI/CD costs
  • Improve developer productivity
  • Maintain test reliability and stability

Key Takeaways

  1. Start Simple — Enable Playwright’s parallel mode with default settings
  2. Measure First — Benchmark your suite before optimizing
  3. Isolate Tests — Ensure each test can run independently
  4. Tune Gradually — Adjust worker count based on actual performance data
  5. Monitor Continuously — Track metrics and optimize over time

Next Steps

Ready to implement parallel testing in your project?

  1. Clone the example repository and explore the working code
  2. Start with 2-4 workers and measure the impact
  3. Gradually increase worker count while monitoring performance
  4. Implement test sharding if you have 100+ tests
  5. Share your results with your team

Additional Resources


Questions or feedback? Leave a comment below or reach out on LinkedIn.

Found this helpful? Share it with your team and help them speed up their test suites too!


by Ralph Van Der Horst

arrow right
back to blog

share this article

Relevant articles

Using playwright model context protocol on my practicewebsite

Intro – What’s MCP & Why LLMs Need Model Context? As AI agents evolve, so does their ability to interact with real-world web apps. Traditional …

Read More

Integrating jira with cucumber and serenity js for enhanced test management

Test automation is crucial to ensuring the quality and reliability of applications. Combining Serenity/JS, Cucumber, and Playwright offers a powerful …

Read More

Why ai makes migrating from payware to open source test tools surprisingly affordable

The Migration Myth That’s Costing You Thousands For years, QA teams have been stuck paying expensive licenses for proprietary test automation …

Read More