How we cut build times in half without changing the stack
For months our CI queue had been the quiet tax on every pull request. Engineers waited twenty minutes for a green check, context-switched away, and came back colder than when they left. We assumed the only real fix was a new toolchain— until we measured where the minutes actually went.
What we found was less dramatic and more useful: most of the wall-clock time lived in avoidable work. Cached layers that never hit, test shards that ran empty, and a dependency graph that rebuilt the same packages three times per pipeline. The stack stayed; the habits changed.
Start by measuring, not guessing
Before we touched a single config file, we instrumented a week of builds. Flame charts from the CI runners told a story the dashboards had smoothed over: compile steps were fine; orchestration was not. A cold cache on every branch build meant we were paying full price for work we had already done on main.
The fastest build is the one you never schedule—and the second fastest is the one that reuses yesterday’s work.
We tightened cache keys so they invalidated on content, not on branch name. Test jobs learned to skip packages whose inputs had not changed. And we stopped serialising steps that had no real dependency on each other. None of it required a new language or a different monorepo tool—only clearer ownership of what “done” meant at each stage.
Two sprints later, median pull-request builds sat just under ten minutes. The stack looked the same on paper. The queue, and the mood around it, did not.