ScreenshotNeo

BlogHow-to

How to Run Browser Tests in Parallel

Run browser tests concurrently without flaky results: isolate test data, choose a safe worker count, and scale across CI machines when needed.

By the ScreenshotNeo team4 October 20268 min read

Run browser tests in parallel by first making each test independent, then increasing concurrency in measured steps. Set a worker limit that fits the CPU, memory, browser processes, and backend capacity available to the run. In CI, begin with a conservative worker count; split work across machines when one host is the bottleneck. Browser contexts isolate browser storage, but they do not isolate shared database records, accounts, files, or external services.

This guide covers Playwright Test, Cypress Cloud, and Selenium Grid. The key distinction is what each approach distributes: tests or files on one host, spec files across CI machines, or browser sessions on remote hosts.

1. Make tests independent before adding workers

Parallel execution exposes hidden dependencies. A test that assumes another test created a record, uses a fixed account, or overwrites a shared download path may pass serially and fail when work overlaps.

  1. Run the suite serially and identify shared accounts, records, files, global settings, and external services.
  2. Give each test unique mutable data, or scope reusable data to a worker that does not overlap with another worker.
  3. Use a distinct output path for each test. Clean up created records and files reliably.
  4. For a genuinely shared resource that cannot handle concurrent access, use a lock or serialize only the smallest affected group.

Playwright creates an isolated BrowserContext for each test, separating cookies and browser storage. That boundary does not extend to your application database, filesystem, external APIs, or shared account state. See the official Playwright isolation guide.

2. Playwright Test: limit worker processes

Playwright Test runs test files in worker processes and runs files in parallel by default. Set a ceiling on workers from the CLI or configuration. Tests in the same file run in order by default; opt into within-file parallelism only when those tests do not rely on shared in-process state.

Set a worker limit from the command line

npx playwright test --workers=4

Use a number that matches the machine running the command. Four is an example, not a universal recommendation. You can also shard a run across CI jobs; for example, this command selects the second of three shards:

npx playwright test --shard=2/3

Configure workers and parallel mode

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

export default defineConfig({
  // Pick a CI limit based on the runner's actual capacity.
  workers: process.env.CI ? 2 : undefined,
  // Enable only when tests across files are independent.
  fullyParallel: false,
});

For a group of independent tests within one file, opt in explicitly:

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

test.describe('independent checks', () => {
  test.describe.configure({ mode: 'parallel' });

  test('first check', async ({ page }) => {
    await page.goto('https://example.com/');
    await expect(page).toHaveTitle(/Example/);
  });

  test('second check', async ({ page }) => {
    await page.goto('https://example.com/');
    await expect(page.locator('h1')).toBeVisible();
  });
});

In parallel mode, hooks run for each test. Do not depend on a hook or test mutating a shared in-process variable for a later test. Consult Playwright parallelism documentation for worker behavior and named locks for resources that must be shared.

Isolate records and files

Derive backend identifiers from test metadata and use Playwright’s per-test output path. The following fixture pattern shows the shape; replace the example API calls with your application’s setup and cleanup endpoints.

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

const test = base.extend<{ recordId: string }>({
  recordId: async ({ request }, use, testInfo) => {
    const recordId = `e2e-${testInfo.testId}`;
    await request.post('/test-support/records', { data: { id: recordId } });
    try {
      await use(recordId);
    } finally {
      await request.delete(`/test-support/records/${recordId}`);
    }
  },
});

test('record appears in the UI', async ({ page, recordId }, testInfo) => {
  const artifactPath = testInfo.outputPath('result.json');
  await page.goto(`/records/${recordId}`);
  await expect(page.getByText(recordId)).toBeVisible();
  // Write any test artifact to artifactPath, not a shared fixed filename.
});

The API routes above are placeholders for your test environment, not Playwright endpoints. If the application safely supports one dataset per worker, use worker identity to assign separate records or accounts. See Playwright’s guidance on data and file isolation.

3. Cypress: distribute spec files across CI machines

Cypress Cloud parallelization distributes whole spec files across multiple CI machines. It requires a recorded run; machines claim available specs and Cypress Cloud uses prior run durations to balance assignment. Specs can run in any order, so one spec must not depend on another having run first.

npx cypress run --record --key="$CYPRESS_RECORD_KEY" --parallel

Configure your CI to launch multiple machines with the same project and run identity, and store the record key as a CI secret. The command’s parallel option controls distribution through Cypress Cloud; it does not turn one local machine into multiple CI machines. See Cypress Cloud parallelization.

Organize specs around coherent flows with reasonably balanced durations. A very long spec can become the final machine’s tail; too many tiny specs add startup overhead. Cypress documents spec-level execution and organization tradeoffs in its test organization guide.

4. Selenium Grid: run WebDriver sessions remotely

Selenium Grid routes WebDriver commands to remote browser instances. It supplies browser capacity across machines; your test runner or CI system still decides how many sessions to create, and the tests still need independent data and accounts.

A client connects using the Grid endpoint configured by your deployment. The URL and capabilities depend on the Grid setup, so use the endpoint and browser options supplied by its operator. Selenium describes the Grid as routing client commands to remote browser instances in its official Grid documentation.

Choose Grid when remote browser availability, machine capacity, or browser and platform coverage is the constraint. Grid does not fix shared test state or choose the right concurrency limit for your application.

5. Choose the right level of parallelism

Approach Work unit Where it runs Key consideration
Playwright workers Worker processes and test files One machine Set a capacity-aware worker ceiling; isolate state.
Playwright sharding Suite shards Multiple CI jobs or machines Choose shard count and per-machine workers together.
Cypress Cloud Whole spec files Multiple CI machines Recorded run required; spec durations affect balance.
Selenium Grid Remote WebDriver sessions Grid nodes on one or more hosts Your runner controls session demand; data isolation remains your responsibility.

Use local workers when one machine has spare capacity and the suite is independent. Use CI sharding or Cypress Cloud when the suite should be divided among machines. Use a remote grid when browser instances or host availability are the limiting resource. Avoid multiplying machines and workers without checking the total capacity demanded.

6. Roll out concurrency safely

  1. Establish a serial baseline. Record elapsed time and failure behavior, and find tests with order or shared-state dependencies.
  2. Fix isolation. Make setup explicit, assign unique test data, scope accounts where appropriate, and use per-test artifact paths.
  3. Increase local workers modestly. Watch CPU, memory, browser startup pressure, backend load, and repeat-run failures.
  4. Start CI conservatively. Playwright recommends one worker as a stability-first CI default. Increase it only when the CI host has room; use sharding for broader parallelism. See Playwright CI guidance.
  5. Scale across machines when the host is the limit. Compare the slowest shard or spec and the cost of extra setup against total run time.
  6. Keep only useful concurrency. Repeat runs and compare both duration and failures. If more workers make the suite slower or less stable, reduce them and investigate contention.

7. Performance, reliability, and cost

Parallelism reduces elapsed time only when work can overlap without saturating a shared resource. Each worker adds browser and application activity. CPU or memory pressure can slow every worker; a backend, test account, rate-limited external service, or database may become the bottleneck first. More CI machines also add orchestration and startup overhead.

Measure the whole run, including setup and teardown, and inspect which worker or spec finishes last. Balanced work matters: static shards can be uneven, while Cypress Cloud uses recorded duration history to assign specs. No general speedup percentage applies across suites; compare your own repeat runs.

Cost depends on the CI minutes or remote browser capacity consumed, plus any cloud service requirements. Keep the smallest worker and machine count that meets the feedback-time goal reliably. A serial or smaller run can be cheaper and more predictable for tests that share a scarce resource.

8. Troubleshooting parallel browser tests

Symptom Likely cause Fix
Tests pass alone but fail in a suite Shared records, accounts, files, or order assumptions Use unique test data and output paths; remove cross-test dependencies.
Failures appear only at higher worker counts CPU, memory, browser, backend, or service contention Lower the worker limit, monitor the bottleneck, then add capacity or reduce per-test load.
Files disappear or contain another test’s output Tests write to the same fixed path Use a test-scoped path such as Playwright’s `testInfo.outputPath()`.
Cypress machines sit idle or finish unevenly Specs are too few, highly uneven, or have expensive per-spec startup Review recorded duration history and organize coherent specs with more balanced durations.
Cypress parallel run is rejected Run is not recorded, record key is missing, or machines do not share the same run configuration Provide the project record key securely and follow the recorded multi-machine setup.
Remote browser sessions queue or time out Grid has fewer available slots than requested sessions Reduce runner concurrency or add available Grid capacity; verify the configured remote endpoint.
Flaky cleanup leaves duplicate data Setup or teardown fails partway through Use unique identifiers, cleanup in `finally`/fixture teardown, and provide a test-environment cleanup routine.
Tests deadlock on shared state Parallel tests contend for a resource requiring exclusive access Use a named lock or serialize only that resource’s tests while removing avoidable shared state.

9. Or skip the browser setup

If the goal is to capture a page as an image or PDF rather than exercise interactive behavior, a screenshot API avoids managing browser workers. ScreenshotNeo takes a screenshot or PDF from one GET request; its API documentation covers the request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Cookie and consent banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, failed loads, and cache hits are not billed; response headers identify the page verdict and billing outcome. An MCP server lets AI agents use screenshot, page-info, and PDF tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. This is for capture workflows, not a replacement for tests that need to click through and assert application behavior.

Sign up for 1,000 free screenshots a month, with no card required.

10. FAQ

Should browser tests run in parallel by default?

Only once each test can run independently. Start with the runner’s conservative CI setting and validate repeated runs before raising concurrency.

Does a new browser context prevent test collisions?

It separates browser cookies and storage, but not shared application data, accounts, files, or external service state.

When should I shard instead of adding workers?

Shard when one CI host is the limiting factor and additional machines are available. Keep per-machine worker counts within each host’s capacity.

Can Selenium Grid make tests independent?

No. Grid supplies remote browser instances; the test suite must still isolate its own data and shared resources.