Parallel testing: what actually determines your wall-clock time

Why adding workers stops helping, how to shard so they are not idle, and what breaks the first time a suite runs concurrently.

By

Running tests in parallel is usually described as if the only decision is how many workers to buy. It is not. Two things cap your wall-clock time no matter how much concurrency you pay for, and most suites hit both long before they run out of workers.

This covers what actually sets the floor, how to split a suite so the workers are not sitting idle, and the specific things that break the first time tests run at the same time.

Last reviewed 1 September 2026.

The two hard ceilings

The first is the part of the run that cannot be parallelised. Checking out the repository, installing dependencies, building the application, pulling a container image, starting a browser: that work happens once, or once per worker, and no amount of concurrency removes it.

Put numbers on it. Say a suite takes 40 minutes: 4 minutes of setup and 36 minutes of tests.

  • 10 workers: 4 + 3.6 = 7.6 minutes. A 5.3x speedup, not 10x.
  • 50 workers: 4 + 0.7 = 4.7 minutes.
  • 100 workers: 4 + 0.36 = 4.4 minutes.

Going from 50 workers to 100 buys you twenty-two seconds. The four minutes of setup nobody ever looked at is now 92% of the run. This is Amdahl's law, and it is the single most useful thing to know about parallel testing, because it tells you when to stop buying workers and start deleting setup.

The second ceiling is blunter: you cannot finish before your slowest single test finishes. If one end-to-end journey takes six minutes, six minutes is your floor at any concurrency. A suite with one monster test and two hundred quick ones does not need more workers, it needs that test split up.

Both ceilings have the same implication. Concurrency is worth buying up to the point where the serial fraction and the longest test dominate, and past that point the money does nothing.

Measure before you buy concurrency

Before changing anything, find out where the wall-clock time actually goes. It is frequently not the tests. A pipeline that reports "40 minutes" often turns out to be twelve minutes of dependency install and image pull, six minutes building the app, and twenty-two minutes of tests.

That distribution changes the plan completely: caching the dependency install is a smaller, cheaper change than any amount of parallelism, and it improves every job rather than just the test job. Parallelising the twenty-two minutes while ignoring the eighteen is optimising the part you happened to be thinking about.

The specific numbers worth having are the total run time, the setup time before the first test starts, the duration of the longest single test, and the distribution of test durations. Those four tell you what your ceiling is and whether sharding will help.

Sharding: count is the wrong unit

Most runners split work by file, round-robin, because it needs no knowledge of the suite. That is fine when durations are even and quietly wasteful when they are not.

Six spec files across two workers. Three of them take 40 seconds each and three take 6 minutes each. Round-robin can hand one worker the three quick files and the other the three slow ones: the first finishes in two minutes and idles, the second runs for eighteen. Your suite takes eighteen minutes with half your capacity doing nothing.

Split the same six files by recorded duration and each worker gets roughly ten minutes of work. Same hardware, same tests, nearly twice the throughput, purely from how the work was divided.

So: shard on timing data, not on file count. Most runners support this, usually by reading a timings file from the previous run, and it is one of the highest-return changes available. Two related points:

  • Shard at the test level, not the file level, if your runner allows it. One file containing forty tests is indivisible under file-based sharding, so it sets a floor of however long those forty take.
  • Keep the timings current. A timings file from six months ago balances the suite you had six months ago.

What breaks when tests run at once

Tests that pass serially and fail in parallel are almost always sharing something. The sharing was invisible while execution was ordered, and it is worth knowing the specific shapes because they are recognisable once you have seen them.

  • A fixed test account. Two workers log in as the same user; one changes a setting or invalidates the other's session. This is the most common one by a wide margin.
  • Fixed identifiers. A test that creates a record with a hardcoded email or reference hits a unique constraint the second time it runs concurrently.
  • "The most recent record." A test that creates something and then asserts on the newest row now sees another worker's row.
  • Fixed ports and fixed paths. Two workers binding the same port, or writing a download or a screenshot to the same filename.
  • Ordering dependencies. Test B passed because test A happened to seed the data it needed. Parallel execution removes the ordering that was holding it up.
  • Shared mutable environment state. Feature flags, system settings, seeded data that one test toggles and another reads.

The general fix is to make identity a function of the run: derive usernames, emails and file paths from the worker or run identifier so no two concurrent tests can collide. Where that is impossible, because a resource is genuinely singular, mark those tests as needing to run alone rather than trying to make sharing safe.

This is also why test independence is worth enforcing before you need it. A suite written to be independent parallelises by changing a number. A suite that was written serially needs auditing first, and the audit is the expensive part.

Parallelism and flakiness

Parallelism makes existing flakiness worse, and it is worth being clear about why, because the usual reaction is to blame the parallelism.

Running twenty browsers on one machine means each gets a fraction of the CPU. Everything gets slower and less predictable: pages take longer to render, animations take longer to settle, requests take longer to return. Any test that was passing because an implicit assumption about timing happened to hold now has that assumption stressed.

Those tests were already fragile. Parallelism did not break them, it revealed them, in the same way that a slower CI machine does. The fix is the same either way, which is to wait for conditions rather than durations, but the practical consequence for parallelism is specific: if you are running workers on one machine, resource contention is a real limit, and past a certain point adding workers makes the suite slower and less reliable at the same time. Distributing across machines, or onto a grid, does not have that problem.

How many workers is enough

Given the ceilings above, the useful question is not "how many can I have" but "where does the curve flatten". A practical way to find it:

  • Measure the serial fraction, meaning setup plus the longest test. That is your floor.
  • Pick a target wall-clock time that is above that floor. If your floor is four minutes, targeting three is buying nothing.
  • Divide the parallelisable time by the time you have left after the floor. That is roughly the concurrency you need.
  • Round up modestly for uneven sharding, then stop.

Our parallel testing calculator does this arithmetic for a suite of a given size and shows where the curve flattens for your numbers.

One caveat that catches people: if your plan allows five concurrent sessions and your pipeline requests twenty workers, fifteen queue. Your effective concurrency is five and the queueing may make the run slower than requesting five would have been. Match the request to what is actually available.

Local workers and a grid

Running workers locally is the right first step and it is free. It stops working for two reasons, and they arrive in this order.

First, contention. One machine has a fixed amount of CPU and memory, and past a handful of browsers you are making every test slower to run more of them, which is the trade described above.

Second, coverage. Local parallelism runs many copies of the same browser on the same operating system. It answers "does this still work" faster; it does not answer "does this work for the person on Safari, or on an older Android device". Those are different questions and only the second one needs a grid.

A grid gives you concurrency without contention, since each session gets its own environment, and coverage at the same time. Your suite points at a remote endpoint instead of a local browser, which is a configuration change rather than a rewrite, whether you are using Selenium, Playwright or Cypress. The same applies to mobile suites on real devices, where the contention argument is stronger still because you cannot run twenty emulators on a laptop at all.

None of which changes the arithmetic at the top of this page. A grid removes the contention ceiling and the coverage limit. It does not remove your setup time or your slowest test, and those are usually what you hit first.

Related reading

Ready to start testing?

Start a free trial