Where Test Parallelization Stops — Why We Capped at 7 Workers, Not 16

"Tests are slow, parallelize them" isn't always the right answer. Six months of data on diminishing returns.

John Baek
John Baek
Founder, CollabOps
Where Test Parallelization Stops — Why We Capped at 7 Workers, Not 16

A year ago our test runs took 17 minutes. Too long. Believing parallelization was the answer, we scaled workers 4 → 8 → 16.

4 workers:   17 min
8 workers:   11 min
16 workers:  10 min

At 16 workers we shaved one minute from 11 to 10. Cost was 4× the 4-worker run. Six months of measurement told us where diminishing returns started.

The four bottlenecks

Tests don't speed up linearly with workers because of one of four bottlenecks.

Bottleneck 1 — shared resource (DB, cache)

If tests share one DB instance, more parallelism makes the DB the serialization point. Our case: a single Postgres. Fine to 8 workers; at 16 the connection pool saturated.

Diagnose: as parallelism increases, measure DB latency. The point where average latency starts climbing is where the bottleneck begins.

Bottleneck 2 — slow setup — fixed cost

Each worker spends fixed time on boot + dependency install + DB migration. If setup is 3 minutes, parallelism doesn't reduce it.

Our case — 3 of the 17 minutes were setup. At 16 workers, still 3 minutes setup. Test execution shrank 14 → 7 min but total 17 → 10 min. Setup defines the floor.

Fix: pre-warmed worker pool, dependency caching, reusable DB snapshots. Cutting setup from 3 min to 30 sec yields the largest improvement.

Bottleneck 3 — uneven distribution — the long pole

Workers don't get equal work. One worker that picks up an unusually slow test group makes all the others wait.

Our data — 15 of 16 workers finished in 4 min, one finished in 9 min. Total time: 9 min. The other 15 spent the difference waiting.

Fix: distribute by historical execution time (not by simple chunking). Or work-stealing — workers that finish early pick up unfinished jobs from others.

Bottleneck 4 — shared external dependency

If tests call external APIs (Stripe sandbox, etc.), the API's rate limit becomes the ceiling.

Our case — payment tests hit Stripe sandbox's 50/min limit. Fine to 8 workers; at 16, rate-limit retries turned the parallelism into a negative.

Fix: replace external API calls with mocks, or request a high-rate vendor sandbox.

Our ceiling — converging at 7 workers

After analyzing the four bottlenecks, we re-measured:

Workers | Total time | Bottleneck
────────┼────────────┼───────────────────────
1       | 38 min     | (none)
2       | 22 min     | (none)
4       | 14 min     | DB pool starting
7       | 9 min      | setup + DB threshold
8       | 8 min      | DB pool saturating
16      | 7.5 min    | setup dominant
32      | 7.5 min    | setup only — parallelism *meaningless*

7–8 workers is the peak efficiency point. Past that, cost rises while time barely moves. Knowing this up front halves CI cost.

To go faster — not workers, setup

Past 7–8 workers, our next move wasn't more workers. It was cutting setup. Four changes:

  1. Reusable DB snapshots — workers don't re-migrate; they mount a pre-built snapshot.
  2. Dependency cache — all workers share the same cache.
  3. Worker warm pool — keep 5 workers always running.
  4. Pre-loaded fixtures — load test data before the first test.

These four took total time 9 min → 4 min. Same 7 workers, 2× saving. Setup is a much bigger lever than worker count.

If you're trying to shorten CI test time by adding workers, first measure which of the four bottlenecks (DB pool, setup, uneven distribution, external rate limits) is active. Scale without measuring and 16 workers gives roughly the same time as 7 at 4× cost.

Tags#testing#ci#performance#parallelization#devops