Test Automation

Parallel Test Execution in CI: Balance Shards and Preserve Results

Parallel Test Execution in CI: Balance Shards and Preserve Results

A combined test report can look healthy while a missing CI job has contributed no results. Adding machines makes this easier to overlook: the visible reports describe the work that arrived, not necessarily the work the pipeline intended to run. A faster result is useful only when engineers can explain which tests ran, which did not, and where the original failures can be inspected.

Parallel test execution runs independent work at the same time. Test sharding divides a selected suite into partitions that CI jobs execute separately; each job may also run several local test workers. Scale those layers deliberately, keeping the selected suite unchanged and making partition completeness a required check alongside test outcomes.

Table of Contents

Freeze the Suite and the Partition Contract

Start with a reproducible definition of the run. All shards need the same source revision, dependency lockfile, test configuration, selection filters, projects, and intended environment. If one job discovers a different inventory because an environment variable changes selection, a correct partitioning algorithm cannot repair the mismatch.

Record a run manifest before distributing work. This is a recommended control to implement in your pipeline, not a promise that every framework generates it automatically. Include:

  • The commit and configuration identity, including browser or project selection and relevant environment dimensions.
  • The canonical selected test inventory, with intentional skips and exclusions distinguished from runnable cases.
  • The planned shard identities, common shard total, and assignment rule or saved assignment lists.
  • The local worker limit, retry policy, and expected report and diagnostic artifact locations.
  • The workflow run and attempt identity, plus an owner for incomplete execution.

Choose a logical execution key that distinguishes real planned work. A stable framework case identifier can be a starting point; otherwise use a validated combination of repository-relative file, full test title, and parameter identity. Include project or browser configuration, deliberate repeat index, and environment when those represent separate executions. A title alone may identify several different cases.

Keep that key separate from the observation key, which adds workflow attempt, shard, and retry attempt. The same case legitimately produces several retry observations. Conversely, including the shard number in the logical key would conceal an accidental assignment of the same case to two shards. Track setup or dependency records separately so expected repeated setup does not look like duplicated business-test coverage.

Check the plan before execution: its assignments should cover the selected runnable inventory without accidental overlap. Then check actual results against that plan. Equal totals are insufficient because a duplicate case can replace a missing one without changing the count.

Set a Concurrency Budget at Both Layers

A shard count describes partitioning, not guaranteed simultaneous execution. The CI scheduler decides which jobs can start, while the test runner controls workers inside each job. GitHub Actions, for example, uses max-parallel to limit concurrent matrix jobs. Playwright's workers setting limits local test-worker processes. These controls govern different resources.

Consider an illustrative configuration with four shards, at most three simultaneous matrix jobs, and two test workers per job. Its configured ceiling is six concurrent test-worker slots. Actual concurrency may be lower because runner capacity, scheduling, setup, or the amount of runnable work limits utilization. This calculation predicts neither browser throughput nor pipeline speedup.

Set the budget against the application environment as well as the CI machines. More jobs may increase database connections, authentication requests, and pressure on shared services even when each runner has spare CPU. Excess local workers can also compete for memory and CPU within one runner. A slow or unstable environment is a reason to investigate capacity, not automatically to add shards.

During the first comparison, hold the worker limit constant and vary the shard count. Record job queue time and resource contention so a scheduler bottleneck is not mistaken for poor test distribution. If jobs must wait in waves, evaluate the actual timeline rather than assuming every partition starts together.

Balance Runnable Units, Not Test Counts

The smallest safely distributable unit determines how much balancing is possible. A scheduler cannot divide a dependent sequence into independent cases merely because another machine is idle. Establish those boundaries before deciding whether to change file organization, runner configuration, or allocation logic.

Respect Files, Fixtures, and Serial Groups

Current Playwright documentation distinguishes file-level sharding from individual-test sharding. Without fullyParallel, whole files are the normal distribution unit; with fullyParallel enabled, individual tests can be distributed across shards. This can reduce imbalance caused by files with very different test counts, but balancing counts does not balance execution duration.

Enabling individual-test parallelism also changes lifecycle assumptions. Playwright runs parallel tests in separate workers, and relevant hooks can execute for each test. A fixture that modifies shared records, assumes a previous case ran first, or performs expensive repeated initialization needs review before changing that mode.

Keep intentionally dependent sequences together until their dependencies are removed. In Playwright serial groups, a failure can skip later tests and the group is retried together. Preserve those outcomes rather than reporting every assigned case as executed. A long indivisible group can remain the limiting shard even after shorter work is redistributed.

Use Duration History With a Fallback

Collect durations at the same granularity you can assign: file, class, test, or approved group. Compare runs with compatible runner resources, environment, and configuration. Stale timings from a different browser or machine can produce a precise-looking but poor allocation.

Where the selected tooling supports historical-duration splitting, use it with matching test identities and timing fields. CircleCI documents this approach, although its available commands and recommendations differ between Cloud and Server. Do not assume that a framework's ordinary count-based sharding option also uses historical duration.

For a custom static plan, a useful heuristic assigns longer estimated units first to the least-loaded eligible partition. Keep required groups intact, define a fallback estimate for new tests, and save the resulting plan so every shard consumes the same assignments. Treat this as an estimate to validate, not an optimal schedule or a replacement for the framework's supported selection mechanism.

Measure the entire feedback path: queueing, setup, test execution, artifact upload, and final aggregation. Repeated setup can consume much of the benefit of finer partitions. Retries can create a long tail on a previously balanced shard. Compare the slowest required completion and total runner use, not only average test duration or one unusually fast run.

Give Mutable State and Evidence Distinct Namespaces

Separate browser contexts do not create separate backend accounts or database records. In an illustrative refund suite, one case changes a finance user's permissions while another checks that user's refund access. Isolated browsers still observe the same server-side change. Give concurrent mutable tests independent accounts or reserved records, or enforce a supported isolation boundary around their work.

Choose names according to resource lifetime. Persistent resources may need the workflow run, run attempt, shard, project or environment, and local worker identity; test-created entities may also need the logical case and retry attempt. A worker index that is unique inside one runner is not a globally unique reservation across several jobs or overlapping workflows.

A database per job, a schema per worker, or namespaced entities can all be reasonable designs if the application respects those boundaries. Avoid cleanup that truncates shared tables or deletes another shard's records. Register scoped teardown and expiry-based cleanup for abandoned jobs. For provisioning and reset details, use the test data management guide.

Apply the same discipline to screenshots, traces, exported files, and reports. Test-scoped output paths prevent local collisions; uploaded artifact names must additionally distinguish the relevant shard, project, and run attempt. Preserve enough metadata to reconnect an attachment to its result after download.

Keep authentication state out of broadly shared diagnostic bundles. Playwright warns that saved browser state can contain credentials capable of impersonating an account. Restrict access and retention, redact sensitive logs, and retain only the evidence an investigation needs. A useful failure report should not become a credential distribution mechanism.

Collect Failures Before Merging Reports

Design artifact collection to survive an assertion failure without converting that failure into success. The official Playwright sharding example disables matrix fail-fast and runs report upload and aggregation under a not-cancelled condition. That supports diagnosis after failed tests; it does not guarantee files survive a runner crash or deliberate workflow cancellation.

For Playwright's blob-report workflow, use this sequence with the framework version pinned by your project:

  1. Enable the blob reporter for each CI shard. Pass a consistent total and the intended shard fraction, such as --shard=1/4 for the first partition of a four-shard plan.
  2. Keep the test command's exit status. Arrange report upload to run after test failure where the CI platform supports it, using distinct artifact names instead of suppressing the error.
  3. Wait for the planned shard jobs, then collect their blob reports and separately required diagnostics. Check expected artifact identities before treating the input directory as complete.
  4. Place the collected blobs in one directory and run npx playwright merge-reports --reporter=html ./all-blob-reports to produce a human-readable report.
  5. Publish that report alongside the completeness verdict, original job outcomes, and retained diagnostic evidence. A successful merge must not replace the required test-status check.

Blob reports are Playwright's merge input, not a universal interchange format for arbitrary test outputs. JUnit XML is a separate reporter format; use an aggregator that understands the format your framework produces. Preserve project and environment identity when combining repeated execution across configurations. Playwright also documents extra merge configuration when inputs come from different operating systems.

Playwright's blob report includes test results and collected test attachments, including traces and screenshot differences. It cannot recover a trace that was never captured, an unflushed report, or an external service log that was never attached. Verify the evidence policy before relying on the merged view. Retain raw shard reports and needed redacted logs so rendering or download problems do not erase the original observation.

An upload condition is only part of that policy. If an older run is cancelled, record that it is incomplete. If a runner disappears before writing its report, retain the available CI logs and classify the infrastructure loss. Merging the remaining reports can still help investigation, but cannot establish that the absent work passed.

Make the Summary Gate Reject Partial Evidence

Require the summary to reconcile planned partitions, observed execution identities, and outcomes independently. Successful report generation establishes that available inputs were rendered; it does not establish full suite execution.

The following is an illustrative four-shard run, not a recorded CI result:

Planned Partition

Received Evidence

Summary Interpretation

Shard 1/4

Report received; assigned cases passed

Accept this partition's evidence after identity checks

Shard 2/4

Report received; assigned cases passed

Accept this partition's evidence after identity checks

Shard 3/4

No report; job cancelled or lost

Mark incomplete; do not infer a pass

Shard 4/4

Report received; assigned cases passed

Accept this partition's evidence after identity checks

Three received reports leave one planned partition unaccounted for. Even if every received result passes, the run must not satisfy a gate that requires complete execution. A missing partition and a missing test identity are separate checks: a report can exist while containing only part of its assigned work.

Compare actual units with the canonical inventory, allowing only explicitly defined multiplicity for projects, repeats, and retries. Flag missing units, unexpected units, and accidental duplicates. Check commit and configuration identity before combining records; a complete report from an older attempt cannot silently fill a gap in the current run.

Keep outcome classification explicit. A skipped case, a setup-blocked case, and a test that never started are not executed passes. Playwright distinguishes cases that pass immediately from cases that fail and later pass on retry. Preserve the first failure and retry evidence even when policy permits the final result. Quarantine requires a recorded scope decision and owner; removing a troublesome case is not proof that sharding preserved the original suite.

Completeness and success are separate decisions. A complete run may fail assertions. An incomplete run may contain no observed failures. Configure the required summary check to reject missing evidence and propagate disallowed outcomes, rather than returning green whenever report generation succeeds.

Hand Off a Reproducible Run

Give the investigating engineer the commit, inventory and assignment manifest, project selection, shard fraction, worker limit, run attempt, relevant environment reference, and access-controlled artifacts. Reproducing a partition requires its selection context as well as its command. Secrets should come from an approved developer setup, not copied CI authentication files.

Assign the workflow owner to missing jobs and artifact collection, the test owner to assertions and fixture dependencies, and the environment owner to contention or shared-state collisions. Preserve the original record before rerunning. A successful rerun is another observation, not a reason to overwrite the failed attempt.

Before making the sharded pipeline the sole required route, compare it with the existing workflow using the same inventory and controlled build and environment. Demonstrate that the completeness check detects an omitted partition and that a failed test still leaves inspectable evidence. These are rollout checks for your implementation, not results established by this guide.

Choose the smallest measured configuration that meets the feedback target without losing execution identity or diagnostic evidence. Increase concurrency only after the team can distinguish a slower shard from a missing one and a complete passing suite from a partial report.

Related

More on Test Automation

Create traceable test cases, prepare release-specific test runs, and keep approved coverage current as the product changes.