How Can You Make a Slow CI Pipeline Faster Without Cutting Tests?
You can make a slow CI pipeline faster without deleting tests by attacking the three biggest time sinks: **run tests in parallel across multiple runners, cache everything that does not change between runs, and only run the tests affected by each change.** Together these typically turn a pipeline that takes tens of minutes into one that finishes in a few — while keeping every test you have.
## First, Measure Where the Time Goes
Before optimising, profile the pipeline. Most teams are surprised by what actually dominates:
- Dependency installation and environment setup.
- Building or compiling the application.
- The test run itself.
- Container image pulls and pushes.
- Idle queue time waiting for a runner.
Optimising the test run when 60% of the time is dependency installation is wasted effort. Break the pipeline into stages and record the duration of each before you change anything.
## Parallelise the Test Run
A single runner executing thousands of tests serially is the most common bottleneck. Split the work:
1. **Shard tests across runners.** Divide the suite into N groups and run each on its own machine. The whole suite then takes roughly its longest shard, not its total.
2. **Balance shards by historical runtime, not file count.** One slow integration file can dwarf a hundred unit tests; balance by measured duration so shards finish together.
3. **Parallelise within a runner too**, using all available CPU cores — but only if your tests are properly isolated, or you will introduce flakiness.
Isolation is the prerequisite here: tests that share a database or global state cannot run in parallel safely. Fixing isolation pays off twice — faster runs and fewer flaky tests.
## Cache Aggressively
Most of a pipeline's repeated work is identical run to run. Cache it:
- **Dependencies** — package manager caches keyed on the lockfile hash, so they only rebuild when dependencies actually change.
- **Build artefacts** — compiled output, transpiled bundles, and Docker layers keyed on the inputs that produced them.
- **Test results** — with the right tooling, unchanged code with unchanged tests need not be re-run at all.
The golden rule of caching is to key the cache on a hash of its inputs. A cache that never invalidates is a correctness bug waiting to ship; a cache keyed too coarsely never gets a hit.
## Run Only What Changed
The biggest wins come from not running tests you do not need to. Two complementary techniques:
- **Test impact analysis** maps which tests exercise which code, so a change to one module only triggers its dependent tests. Many build systems and frameworks support this.
- **Path-based filtering** runs documentation-only changes through a trivial pipeline, and skips the mobile test suite when only backend files changed.
The important caveat: **always run the full suite before merging to the main branch or deploying.** Selective execution is for fast feedback on branches; it is not a substitute for a complete pre-deploy gate, because impact analysis can miss indirect dependencies.
## Fail Fast and Order Smartly
Developer time is wasted waiting for a pipeline that was always going to fail. Reorder for speed:
- Put **linting and fast unit tests first** so obvious mistakes fail in under a minute.
- Run **slow E2E tests last**, only after the cheap checks pass.
- Enable **fail-fast** so the pipeline stops as soon as a blocking stage goes red, freeing runners.
## Right-Size Your Infrastructure
Sometimes the pipeline is slow because the runners are starved. Check:
- Are jobs queuing because there are not enough runners? Add capacity at peak times.
- Are runners under-powered for compilation or memory-heavy tests? A bigger machine can be cheaper than the engineering hours lost to waiting.
- Are you pulling huge container images every run? Slim them down and cache layers.
At neart.ai we treat pipeline speed as a first-class quality concern, because a fast feedback loop is what makes a comprehensive test suite sustainable rather than something engineers route around.
## Practical Takeaway
Profile your pipeline first, then parallelise tests across balanced shards, cache dependencies and build artefacts keyed on their inputs, and use test impact analysis for fast branch feedback — while always running the full suite before deploy. You keep every test and still get your feedback loop back under a few minutes.