Azure DevOps 2 min read 1

We Cut CI From 22 Minutes to 6, and Most of It Was Waiting

A teardown of a slow pipeline: what the wall-clock was actually spent on, and the four changes that bought back sixteen minutes.

We Cut CI From 22 Minutes to 6, and Most of It Was Waiting
Azure DevOps · October 19, 2026

Twenty-two minutes from push to green. Nobody had measured where it went, so nobody had fixed it. When we finally instrumented the pipeline, the result was embarrassing: about four minutes of real work and eighteen minutes of waiting.

Where the time actually went

Stage Before After What changed
Checkout + setup 3m 40s 0m 50s Dependency cache
Install 6m 10s 1m 20s Lockfile-aware cache key
Lint + typecheck 2m 05s 1m 10s Ran in parallel with tests
Unit tests 5m 30s 1m 40s Sharding across 4 runners
Build image 4m 20s 0m 55s Layer cache + multi-stage
Total 22m 6m

The four changes, in order of payoff

1. Cache the dependencies, keyed on the lockfile. Installing from scratch on every run is the single most common waste in CI. Key the cache on the hash of package-lock.json (or go.sum, or poetry.lock) so it invalidates exactly when it should.

2. Parallelise anything independent. Lint, typecheck and unit tests do not depend on each other. Running them sequentially is a choice, not a law. Fan out, then join.

3. Shard the test suite. If tests take five minutes, four runners take about eighty seconds. Most frameworks support sharding natively now; GitHub Actions matrix builds make it a few lines of YAML.

4. Stop rebuilding the image from cold. The image build was re-downloading base layers every run. A registry mirror plus a layer cache cut it from four minutes to under one.

The part nobody expects: queue time

We measured runner queue time separately and found another three minutes hiding in "waiting for an available runner" during peak hours. Autoscaling the runner pool removed it entirely — and it does not show up in the per-stage table above, because it happens before stage one.

If you cannot see queue time, you are measuring the pipeline, not the developer's wait.

Six minutes is not a magic number. The point is that the wait was measurable, and once it was measurable it was mostly trivial to remove. Measure first; the fix is usually boring.

PB
Written by

Parul Bedi

Part of the Azure Gravy writing team.

Up next

The Four Golden Signals, and the Two Everyone Forgets

Azure October 23, 2026 1 min read 2 comments
Open post

Conversation

1 comment
  • SI Sana Iqbal
    Queue time being invisible is such a good catch. We measured per-stage for months and kept missing the three minutes of runner wait in front of it.

Leave a comment

Replying to a comment.
Be kind. Markdown is not supported in comments.
Keep reading

Related posts

All posts