Continuous Integration
AI DevOps Agent
Blog →
Continuous Integration

Why Your Test Suite Randomly Takes 15 Minutes Instead of 2 | Harness Blog

A hands-on investigation into CI performance and reliability, including Test Intelligence, quarantine, caching, and parallel execution.

TL;DR

  • Test time is not pipeline time. The lab saw a 52x gap between the Vitest run (~2s) and the Harness pipeline (~100s), with initialization alone swinging from 6s to 42s between executions. Measure where the time actually goes before optimizing the test runner.
  • A green rerun is not a fix. Retries turn 2 passed / 2 failed into 4 passed on paper, but they hide how often a test is unstable. Quarantine (via the Harness Test Analysis Plugin) is the honest middle path: known flakes stop blocking merges, unknown failures still fail the build.
  • Fix state, then shard, then skip. Shared mutable state turned 4 passing tests into 2 failures; isolation fixed it without weakening assertions. Only after tests are deterministic do parallel shards and Harness Test Intelligence (which skipped 4 of 21 unrelated tests on a scoped code change) pay off.

CI test suite performance is deceptive. A pipeline can have tests that finish in under two seconds and still feel painfully slow. Worse, it can be slow and unreliable: developers wait for a result, see a failed test, press rerun, and hope the next attempt is green.

We wanted to understand where that time goes and what actually helps. So we built a small CI performance lab on Harness Cloud using Node.js, TypeScript, and Vitest, then added a separate Java/JUnit project to exercise Harness Test Intelligence (which analyses your code changes and skips tests that cannot be affected). The goal was not to produce an impressive percentage. It was to recreate the kinds of problems engineers encounter and measure the difference between faster tests, faster pipelines, and more trustworthy feedback. 

This is a controlled lab, not a production workload. That distinction matters, because the most useful findings were about understanding where the time actually goes before changing anything. If you are a platform engineer or tech lead investigating why CI takes longer than the test suite, or why flaky tests keep blocking merges despite retries, this is the walkthrough.

1. Test time vs pipeline time: the 52x gap nobody measures

The first measurement was test execution time versus the entire Harness pipeline.

The lab ran five baseline executions of a 52-test Vitest suite, followed by five executions with synthetic delays introduced inside the tests. The delays stood in for slow work; they were not real network dependencies.

Workload Average Vitest time Average pipeline time
Baseline (5 runs) 1.902s 101.2s
Synthetic delayed tests (5 runs) 4.874s 78.6s

The tests became about 2.56 times slower, yet the overall pipeline averaged less time in the delayed group. That is not an optimization result. It is a warning against treating the duration of one step as the duration of CI.

Two executions illustrate the mismatch: both spent about 4.9 seconds in Vitest, but one pipeline took 118 seconds, and another took 44. Their initialization steps alone took 40 and 6 seconds, respectively.

Where Harness helps: Its execution timeline and step logs let you inspect initialization, checkout, dependencies, test execution, and reporting separately. Before changing the test runner or buying more compute, find which portion of the pipeline is actually consuming time.

Image 1.png

2. Flaky tests, retries, and why green does not mean fixed

A green rerun feels reassuring. It should not be confused with a fix.

In the flaky-test experiment, the same four-test scenario initially produced two passes and two failures. Enabling a retry produced four passing results after one additional test attempt. Replacing the problematic patterns with corrected tests gave four passes without relying on a retry.

Harness run What happened
#14, flaky tests 2 passed, 2 failed
#16, retries enabled 4 passed after 1 retry
#17, corrected tests 4 passed without the intentional flaky behaviour

A timing-sensitive assertion used an unrealistically tight three-millisecond budget. That makes the outcome depend on the execution environment rather than application correctness. Retries can reduce disruption, but they also conceal how often a test is unstable. The broader question of what to trust in AI-generated test output is one we explored in Catch AI Regressions Before They Ship with AI Evals in CI/CD.

DeNA documented a much larger version of this problem: an E2E CI suite that sometimes required repeated runs, with pipeline times over 20 minutes, before the team tackled sources of nondeterminism and reported bringing the run down to around seven minutes. That is an independent engineering example, not a Harness result. Read DeNA's account.

Image 2.png

Quarantining known flaky failures without ignoring every failure

When fixing an unreliable test immediately is not practical, the next question is how to avoid blocking every build without allowing genuine regressions through.

For that, we used the Harness Test Analysis Plugin. It reads JUnit XML reports and compares failed test names against a quarantine list. The test-running step is configured to ignore its own failure so that the analysis step can make the policy decision.

The lab tested two reproducible scenarios:

Scenario Test results Test Analysis result
A: Known failure only 3 passed, 1 quarantined failure Passed
B: Known + unexpected failure 3 passed, 1 quarantined + 1 non-quarantined failure Failed, as intended

In Scenario A, the plugin explicitly logged that the failing test was quarantined. In Scenario B, it identified a test not on the quarantine list and rejected the execution. Quarantine is selective, not a blanket instruction to ignore errors.

The v1 pipeline used a container-based run step after the test step:

plaintext
- id: testAnalysis
  name: Test Analysis
  run:
    container:
      image: plugins/test-analysis:latest
    with:
      test_globs: ci-performance-lab/reports/quarantine.xml
      quarantine_file: ci-performance-lab/quarantine.yml
      fail_on_quarantine: true

The preceding run-test step generated the JUnit XML and set on-failure to ignore. We also added a runtime boolean input to enable Scenario B and analyze a separate quarantine-demo.xml report. The official documentation illustrates a type: Plugin step; we validated the container-based v1 pattern above.

One important caveat: to make the policy test reproducible, the final quarantine example uses a deliberately failing assertion rather than waiting for a real intermittent timing failure. It validates quarantine handling, not automated flaky-test detection. Real quarantined tests should have owners, expiry dates, and a plan to fix them.

Image 3.png

3. Shared state: why tests pass alone but break together

Sometimes the problem is not time at all. It is state.

The next experiment introduced four tests that shared a mutable in-memory order store. In the state-polluted version, two of four tests failed because earlier tests left data behind. With fresh application state for each test, all four passed under the same assertions.

Test setup Result
Shared mutable state, execution #18 2/4 failed
Isolated state, execution #19 4/4 passed

This was a sequential test-isolation experiment, not a concurrency stress test. Harness exposed the assertion failures and made it straightforward to compare executions side by side. The fix was in test setup: give each test a clean state rather than weakening its expectations.

RunPod's engineering team reached a related conclusion at a much larger scale. Its integration tests used the same account, preventing safe parallel execution. After giving workers isolated identities and restructuring long-running test files, RunPod reported an early production merge-queue median improvement from 10.1 to 4.5 minutes. Read the case study.

Fix shared-state assumptions before applying more parallelism. Faster concurrency against shared data simply produces failures faster.

Image 4.png

4. Parallel shards and the initialization overhead that ate the speedup

We split the same 16 tests across two parallel Harness steps. The first arrangement put 11 tests on one shard and five on the other. A more balanced arrangement distributed seven and nine.


Measurement Unbalanced Balanced
Tests across shards 11 / 5 7 / 9
Slowest shard's Vitest time 1.23s 1.20s
Harness parallel-group duration 5s 5s

The allocation became more even. At this scale, the parallel group finished in the same time, which tells you something useful: for small suites, initialization overhead dominates and sharding pays off only once test volume is large enough to outweigh it. Harness makes that trade-off visible by reporting each shard's duration independently.

For a larger example of what can happen when the slowest shard is the bottleneck, Flakiness.io reported reducing WordPress Gutenberg's Playwright runtime from 35 to 23 minutes by balancing shards using test-duration data. Read the Gutenberg case study.

Where Harness helps: Parallel steps provide a place to run independent test partitions and inspect their individual durations. Whether balancing helps depends on the shape and size of the workload, and on what dominates the overall pipeline. The Introduction to Parallel Steps university course walks through the configuration in detail.

Image 5.png

5. Dependency caching: what a cache hit actually costs

Dependency caching is often an early CI optimization, but it needs a real before-and-after measurement.

We enabled Harness Cache Intelligence for npm and learned an unexpectedly practical lesson: verify the package manager's actual cache location rather than assuming the home directory resolves as expected. In this Harness Cloud environment, the relevant npm cache path was /harness/.npm. Once the correct path was used, a cache was saved and restored successfully.

Measurement Initial successful save, #25 Warm restore, #26
Dependency install step 4s 3s
Cache restore step Initial miss 8s
Entire pipeline 70s 67s

The warm run installed dependencies one second faster, with eight seconds spent in cache restore. The real value here is visibility: Harness breaks out restore time, install time, and save time as separate steps, so you can see exactly whether caching pays for itself in your dependency profile rather than guessing.

Where Harness helps: Cache Intelligence can persist and restore common dependencies. The decision to keep a cache policy should still be based on representative warm, cold, and changed-dependency builds, not on the existence of a cache hit alone.

Image 9.png

6. Test Intelligence: skipping the tests a code change cannot reach

This is where the lab went beyond inspecting pipeline timings to show selective test execution in a working Harness configuration.

A separate Java/Maven/JUnit project provided 21 tests distributed across three test classes:

plaintext
OrderServiceTest: 12 tests
plaintext
DiscountEngineTest: 5 tests
plaintext
TaxCalculatorTest: 4 tests

The idea was straightforward: change the discount code, then check whether Harness Test Intelligence avoids running the unrelated tax tests.

The Test Intelligence configuration

  1. Create a Java/Maven Build stage on Harness Cloud Linux, with code cloning enabled into the default /harness workspace.
  2. Add Setup Java and a build step that executes cd ti-java-demo && mvn -B -DskipTests package.
  3. Add Test on Linux as a run-test step, executing cd ti-java-demo && mvn test.
  4. Enable Test Intelligence in that test step and point JUnit reporting to ti-java-demo/target/surefire-reports/*.xml.
  5. Run a baseline, then push a production-code change and run again against the updated branch.

The Test Intelligence documentation describes the Test step and the required default checkout workspace. The Introduction to Test Intelligence university course covers setup end to end. Language/framework support matters, which is why this demo uses Java/JUnit rather than treating our separate Vitest suite as automatically supported.

What Harness actually ran

Result Baseline execution #3 Modified-code execution #4
Total tests represented 21 21
Executed and passed 21 17
Skipped by Test Intelligence 0 4
Failed 0 0
Full pipeline duration 56s 159s

After a change confined to DiscountEngine.java, Harness selected the 12 OrderServiceTest and five DiscountEngineTest cases, while skipping all four TaxCalculatorTest cases. The execution logs also showed that Harness generated and uploaded a non-empty call graph with 52 nodes.

That is a 19% reduction in executed test count (4 of 21) on a 21-test suite. The time saving at this scale is negligible, which is expected: Test Intelligence is built for repositories where the full suite takes minutes, not seconds. The verified result is that Harness made the intended change-aware selection, and that selection scales with your test surface.

Image 8.png


Applying this sequence to your own pipeline

The temptation is to turn on every optimization

at once. A narrower sequence works better: first separate test time from pipeline overhead, then fix nondeterministic and shared-state failures, then measure expensive work worth caching or parallelizing. Once the tests and execution model are reliable, use change-aware selection to avoid work that genuinely is not relevant.

Faster feedback and trustworthy feedback are different outcomes. Harness provided the observability to find slow steps, the quarantine boundary to separate known flaky failures from real regressions, and the call-graph analysis to skip irrelevant tests. The fixes themselves came from what that visibility revealed: bad assertions, shared state, startup overhead. That is the pattern. Harness shows you where the time goes; you decide what to change.

If you are investigating CI test suite performance on a slow or unreliable pipeline, start with one execution: identify where the time goes, check which failures are genuinely nondeterministic, and change one thing at a time. Then compare the result in the pipeline execution history rather than trusting a percentage on its own. Once CI feedback is fast and trustworthy, the next constraint is usually deployment: that is where Harness CD pipelines pick up.

If you are new to Harness CI, Building Your First CI Pipeline covers the setup from a fresh Git repo through to a Docker image push. Then fork the lab and run it against your own pipeline: harness-community/harness-sample-apps, branch feat/ci-performance-lab.


Lab notes: All experiments used a controlled sample application in harness-community/harness-sample-apps on branch feat/ci-performance-lab. The flaky-quarantine policy demonstration used deliberately failing assertions; the Java Test Intelligence demonstration used a separate JUnit fixture. Measurements are from the specified Harness executions, not benchmark claims for other projects or environments.


← Previous:
Next: →‍

Related Resources

Get Started

Get Started with Harness AI

Try the full platform free. No module restrictions, no credit card.

Shibam Dhar
Developer Relations Engineer
Shibam Dhar is a developer Relations professional with years of experience advancing developer experience, education, and community engagement.
shibam-dhar
Shibam Dhar
https://www.linkedin.com/in/shibamdhar
https://x.com/itsme_shib