AI agents ship code faster than CI can validate it. That’s the new bottleneck. Every pull request still has to run the full pipeline, so as agent-assisted development accelerates, CI queue time grows and infrastructure costs climb with it.
Linear’s CTO assigned a ticket earlier this year with two goals: cut CI costs and make CI faster. Despite the test suite nearly quadrupling since January, the team brought pull request wait time down from over 6 minutes to just over 5, while cutting runner time per test roughly in half. Here’s the full playbook.
1. Upgrade the infrastructure first
The earliest gains came before touching a single workflow file. Linear moved workloads off GitHub Actions to third-party runners with faster CPUs, higher-performance storage, and better cache infrastructure. In a like-for-like comparison of the two days on either side of the switch, jobs ran 34% faster on average. Some workloads, like tsc, dropped 52%.
Separately, switching to tsgo, the native TypeScript compiler, cut the weekly median of the tsc check by 73%. That single change moved the bottleneck off typechecking entirely.
Drop the type checker from lint runs
A handful of custom ESLint rules depended on TypeScript type information, which forced every lint run to build the full type graph first. That made linting one of the most memory-intensive jobs in the pipeline.
The fix was rewriting those rules to use static analysis over the abstract syntax tree instead. ESLint no longer needed TypeScript at all. API lint time dropped 68%, and full-repository lint time fell 55%. Memory usage dropped substantially as well. As a side benefit, the cleaner rules made a later migration to Oxlint straightforward, which further reduced CI runner-minutes on linting.

⏱️ 2. Optimize the jobs on the critical path
With individual checks running faster, the team zoomed out and looked at CI as a system. The small jobs that sit in front of everything else turned out to matter the most. None of the eight API test shards can start until those gate jobs finish, so even a few seconds of drag there multiplies across every run.
Fetch only what each job actually needs
Change-detection jobs were checking out the full working tree even when they needed only a small subset of files. Capping the fetch depth took the slowest of these gates from 94 seconds to 20. Removing checkout entirely from jobs that never needed a working tree brought those from 27 seconds to 7. For commit push and merge-queue events, a sparse, blobless checkout with limited history saved another roughly 11 seconds on top.
The median duration of the change-detection job fell from 26 seconds to 8. The p90 dropped from 31 to 12. The slowest recorded run went from 138 seconds to 37.
Build a resilient checkout action
After the runner migration, checkout times started hanging. Third-party runners sit outside GitHub’s network and rely on a direct IP link that showed intermittent degradation. A stalled fetch could hold up the entire CI run.
The team replaced actions/checkout with a composite action that retries with backoff, sets GIT_HTTP_LOW_SPEED_LIMIT and GIT_HTTP_LOW_SPEED_TIME so a stalled connection aborts after about 30 seconds instead of hanging, and uses a checkout cache that keeps a persistent git mirror on a sticky disk. Stalled critical-path jobs dropped sharply.
Move non-blocking work off the critical path
Cache markers were being written as part of the final check before merging, which meant a pull request could sit in the merge queue even after its tests had passed. Moving that write into a job that runs after the test shards finish but gates nothing shaved 42 seconds from the merge path for every API pull request and merge-queue entry.
️ 3. Cut repeated setup overhead
Setup overhead compounds fast. A job that does 10 seconds of useful work but spends 2 minutes booting, installing packages, and fetching dependencies is mostly waste. Three changes addressed this directly.
Preinstall shared dependencies in the CI image
Each API test shard was spending 7 to 8 seconds installing the same Postgres client via apt on every run. Moving it into a small CI base image containing Node and the client eliminated that cost per shard. Native build headers were added to the image later after discovering that downloading them during setup could occasionally hang.
Install only what each job needs
Linear’s codebase is a monorepo managed as a pnpm workspace. The API test workflow was installing the entire workspace even though it only needed the API package and its dependencies. Restricting the install to the API package cut pnpm install from 44-73 seconds to 16-18 seconds.
Test caching before assuming it helps
The team tested caching node_modules and found it was slower, not faster. The cache key depended on a frequently changing lockfile, and even a cache hit took about 28 seconds to restore, compared with roughly 7.5 seconds for a filtered install. The cache was adding save time and variability without a net benefit. They removed it.
Together, these three changes reduced per-shard setup time by roughly 44%, from 110-140 seconds down to 67-73 seconds.
Skip setup when inputs haven’t changed
Some setup only needs to repeat when its inputs change. API containers were replaying the full database migration history on every run, even when a PR hadn’t touched the schema. Switching to a generated schema snapshot and bootstrap file cut database setup from roughly 12 seconds to 1-2 seconds per container.
Batch short checks into fewer jobs
Seven independent checks were each starting a runner, checking out the repo, and installing dependencies before doing only seconds of useful work. Consolidating them into two jobs and running the seven tasks concurrently inside those jobs reduced the number of times the team paid setup overhead from seven to two. Based on June usage, that single change saved roughly 87,000 runner-minutes per month, equivalent to 11.8% of total CI usage.

4. Make test execution more efficient
With fixed per-shard costs down, more aggressive parallelization became practical. The API suite was the largest and most frequently executed part of the workflow, so improvements there had an outsized effect on merge time.
Balance shards by test file size
Vitest distributes work by file rather than by individual test duration. A few unusually large test files were dominating single shards and holding up completion of the entire suite while other shards sat idle.
The fix was splitting those large files into smaller, more focused files. The team then evaluated different shard and runner configurations. Moving from four shards to eight made the critical job roughly 19% faster and 19% cheaper in the initial benchmark. A week after the change, the slowest shard dropped from 5.25 minutes to 4.33 minutes.
Share module state where it’s safe
Vitest normally isolates every test file. For Linear’s suite, that meant rebuilding the entity, GraphQL, and decorator graph in each test shard from scratch. They introduced an opt-in Vitest project with isolate: false, allowing safe files to share a module registry within each worker.
This was the single largest performance improvement in the entire effort, worth roughly 17% in monthly savings at their volume. The slowest shard fell from roughly 300-379 seconds to about 195 seconds. Total API-shard runner time dropped from about 32.8 to 22 minutes per run.
The correctness risk was real. Eligibility required an explicit opt-in comment on every file. Necessary teardown for shared state had to be added. Files using fake timers or shared state in ways that couldn’t be safely untangled stayed in the isolated project. And because agents now write the majority of tests at Linear, the team updated agent skills to follow the same performance opt-in by default.
Why sharding limits depend on setup time
Doubling the shard count also doubles workflow time spent on setup. At 110-140 seconds per shard, eight shards would have spent 15-19 minutes of runner time on setup alone, more than the tests themselves. Setup is now around 40 seconds, so eight shards spend less total setup time than four shards did before the optimizations, while parallelizing tests twice as far.
Before setup optimizations: 4 shards, 8.3 minutes on setup. After: 8 shards, 7.5 minutes on setup. The improvements compound rather than add.
The compounding result
Without this effort, today’s test suite would take roughly 11 minutes to clear CI, close to double the current wait. The team is currently adding roughly 2,000 tests a week. Keeping CI fast as the codebase grows will require continued work using the same approach: identify the bottleneck, measure before and after, and apply the fix that improves the system rather than just one job in isolation.


