> ## Content Index
> Fetch the complete content index at: https://varops.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# A slow CI and an expensive CI are different problems. Here’s what Linear’s numbers show.
- URL: https://varops.com/a-slow-ci-and-an-expensive-ci-are-different-problems-heres-what-linears-numbers-show/
- Published: 2026-09-22T14:04:29.000Z
- Updated: 2026-09-22T14:04:29.000Z
- Description: Linear quadrupled its test suite and still cut the wait on a pull request. The change-by-change bill shows why more shards make one number worse until the boring fixed cost has been paid down first - and why the whole thing now depends on a note filed where the agents writing the tests will read it.
- Author: Rex Factor
- Tags: Proof of Work

*Two numbers hide inside the phrase “CI is slow,” and almost nobody separates them. One is the wait a developer or an agent sits through. The other is the meter.* [***Rex***](https://varops.com/columnist/rex/) *has Linear’s year of receipts on both, change by change, with a figure attached to each. The part worth your four minutes is not that they made it faster. It is that the obvious fix made one number worse until they had already paid down something boring, and that the whole thing now depends on a note filed where the agents writing the tests will read it. —* [*Muximus*](https://varops.com/columnist/muximus/)

---

Linear’s test suites almost quadrupled this year. The wait on a pull request went down anyway, from more than six minutes to just over five, and [runner time per test was cut roughly in half](https://varops.com/what-are-you-actually-paying-for/).

That is the outcome. The work that proves it is unusually well documented, and there is one moment in it worth more than the headline.

On 2026-09-21 Linear engineer Mufeez Amjad [published an itemized account](https://linear.app/now/ci-bottleneck-reworked?ref=varops.com) of the year, change by change, with a figure attached to each one. Somewhere in the middle sits a chart with two lines on it. Coverage climbs. Machine time per test spikes. The caption explains the spike in one clause: they had added test shards, “which shorten the wait and costs more machine time.”

The standard fix for a slow pipeline had just made the pipeline more expensive, and the team said so in a caption rather than cropping the chart.

Everything that follows is Linear measuring Linear. No independent party has reproduced any of it, and the post carries no dollar figures at all - runner-minutes and percentages of a base nobody outside the company can see.

## Two numbers wearing one word

Linear states what it optimized for directly: “how long a PR waits on CI and how much runner time it consumes.”

Those are different quantities with different owners. Wait time is latency - what a developer, or an agent, sits through before learning whether the change survived. Runner time is the meter, the number finance sees. A team that says “CI is slow” usually means the first and gets billed for the second, and the standard remedy for the first is to buy more of the second.

What made that trade stop working at Linear is stated plainly. Agents made shipping code exponentially faster; [validating it did not keep up](https://varops.com/jane-street-spent-25-years-saying-formal-methods-werent-worth-it-agentic-coding-changed-the-receipt/). Linear is adding roughly 2,000 tests a week, and says agents now write the majority of them. That claim arrives without a figure behind it, which is worth holding onto, because the rest of the post is nothing but figures.

Shipping got cheap. Checking did not. The constraint moved onto the one part of the system that [charges by the minute](https://varops.com/the-model-sets-the-success-rate-the-harness-sets-the-bill-heres-where-the-money-goes/).

## How they pulled it off

Start with the pair of numbers that carries the whole argument.

Adding shards is the obvious fix for wait time, and it is why the machine-time line spiked. Split the suite eight ways and each piece finishes sooner. Pay eight times the fixed setup cost to get there.

Then Linear doubled the shards again, to eight, and setup time came out *lower* than before: 8.3 minutes on the old four-shard configuration against 7.5 on eight - the second figure measured after a year of setup cuts, not as a consequence of the sharding. The critical job came out roughly 19% faster and 19% cheaper in their initial benchmark. A week later the slowest shard had fallen from 5.25 minutes to 4.33.

Same action. Opposite bill. The difference is everything they did in between.

Linear shows the arithmetic. At 110-140 seconds of setup per shard, eight shards would have burned 15-19 minutes of runner time on setup alone - more, the post notes, than the tests themselves. Cutting per-shard setup by roughly 44%, to 67-73 seconds, is what turned an expensive move into a cheap one.

## By the numbers: what paying down fixed cost looked like

None of this is interesting on its own, which is exactly why it usually does not get done. Each item below would lose a prioritization meeting to almost anything.

- **Postgres client, 7-8 seconds per shard per run.** Installed with apt on every run. Moved into a CI base image with Node already in it.
- **pnpm install, 44-73 seconds to 16-18.** The workflow was installing the entire pnpm workspace when the API package and its dependencies would do.
- **Database setup, \~12 seconds to 1-2 per container.** API containers were replaying the full migration history every run, including runs that changed no schema. Replaced with a generated schema snapshot and bootstrap file.
- **Seven jobs to two, \~87,000 runner-minutes a month.** Seven independent checks were each booting a runner, checking out the repository and installing dependencies to do a few seconds of real work. Consolidated into two jobs running the seven tasks concurrently. Linear puts the saving at 11.8% of total CI usage, based on June’s figures.
- **Change-detection gate, 94 seconds to 20.** These jobs decide what runs next, so nothing else starts until they finish - none of the eight API shards can begin while one is still reading a diff. They were checking out the full working tree to do it. Capping fetch depth fixed the worst of them; removing checkout entirely from jobs that never needed a working tree took others from 27 seconds to 7\. Median for the change-detection job went from 26 seconds to 8, p90 from 31 to 12, worst observed run from 138 to 37.

Then three swaps further up the stack: third-party runners made jobs 34% faster on average, measured as a like-for-like comparison of the two days either side of the switch, with `tsc` down 52%; [tsgo](https://github.com/microsoft/typescript-go?ref=varops.com), the native TypeScript compiler, cut the weekly median typecheck by 73%, enough to move the bottleneck off typechecking entirely; and rewriting type-aware lint rules as pure AST analysis cut API lint time 68% and full-repository lint 55%, which also made a later move to [Oxlint](https://oxc.rs/docs/guide/usage/linter?ref=varops.com) straightforward.

Linear does not name the runner provider. That 34% is a real measurement of something nobody else can buy by name.

One detail in that section is worth copying verbatim by anyone moving off hosted runners this quarter. Third-party runners sit outside GitHub’s network and reach it over a direct IP link. Linear’s provider traced intermittent checkout hangs to degradation on that link, and since several workflows open with a checkout, a stalled fetch could idle an entire run. The fix was a composite action replacing `actions/checkout` that retries with backoff and sets `GIT_HTTP_LOW_SPEED_LIMIT` and `GIT_HTTP_LOW_SPEED_TIME`, so a stalled connection aborts after about 30 seconds instead of hanging.

## The cache they measured and deleted

A short aside that proves nothing about the argument and stays anyway.

Linear tested caching `node_modules` and found it was faster to rebuild. A cache *hit* took about 28 seconds to restore, against roughly 7.5 seconds for a filtered install, with a cache key tied to a frequently changing lockfile. The verdict in the post: it was “adding save time and variability without giving us any discernible advantage.”

How many teams are carrying a cache step nobody has timed since the day it was added? The belief that caching is free is the same belief that sharding is free. Both survive because the number that would kill them was never taken.

## The expensive one, and what it cost to make safe

The largest single win was not infrastructure at all.

[Vitest](https://vitest.dev/?ref=varops.com) isolates every test file by default, which for Linear meant rebuilding the entity, GraphQL and decorator graph inside every shard. They introduced an opt-in Vitest project with `isolate: false`, letting eligible files share a module registry within each worker.

Worth roughly 17% in monthly savings at their volume. The slowest shard fell from around 300-379 seconds to about 195\. Total API-shard runner time dropped from about 32.8 minutes per run to 22.

Linear also calls it “the optimization with the highest correctness risk,” and the guardrails are the part an operator should copy before the speedup. Eligibility is explicit: an opt-in comment is required on every file, rather than a repository-wide setting. Teardown was added for shared state. A handful of files using fake timers, or sharing state in ways the team could not untangle safely, were left in the isolated project.

Nothing was made faster by default. Files had to ask.

That decision is what makes the next one necessary.

## The constraint has to live where the code is written

Because agents write most of Linear’s tests, an opt-in comment convention policed by humans in code review would have decayed within weeks. So Linear updated its agent skills to account for the performance opt-in, “so that generated tests follow the same constraints by default.”

An optimization the code generator does not know about is not an optimization. It is a temporary state with a half-life set by how fast the generator writes.

That is the part that transfers even though the percentages do not. These are one TypeScript monorepo’s numbers, on pnpm and Vitest and Postgres, and Linear’s own hedge - that “many of these optimizations apply across languages and toolchains” - comes with no evidence for which ones. What does carry is the order of operations the sequence implies: measure wait time and runner time as separate numbers, strip the fixed per-job cost before buying parallelism, and write the resulting constraint into whatever is generating the code. Linear does not state that as a rule; it is the shape its year took.

Which leaves one number doing more work than it can support.

Set against job-level wins of 34%, 52%, 68% and 73%, a minute off the wait looks thin. Linear does answer that: without the year’s work, it says, today’s suite would take roughly 11 minutes, close to double what developers wait now. That is the reconciliation, and it is an estimate of a pipeline that never ran, with [no method shown for how it was reached](https://varops.com/that-2x-developer-productivity-number-who-owns-it-and-why-nobody-can-verify-it/).

The measured figure is the five minutes. The rest is a counterfactual, and counterfactuals are where infrastructure work goes to be justified after the fact. Linear has published more of its working than almost anyone else will this year, and that last number is still the one nobody can check.