Linear’s CTO filed an issue titled “CI costs are high” and asked for faster CI while it was being fixed. The result, written up by Mufeez Amjad, is a rare CI postmortem with numbers: despite the test suite almost quadrupling in a year, PR wait time went from over 6 minutes to just over 5, and runner time per test roughly halved.
The organizing idea is that CI cost is dominated by fixed per-job overhead — runner boot, checkout, dependency install, container provisioning. So the wins came from spending less of it:
- Third-party runners instead of GitHub Actions: 34% faster on average like-for-like,
tscdown 52% tsgocut the weekly median typecheck by 73%, taking it off the critical path entirely- Rewriting custom lint rules as AST-only (no type graph) let ESLint drop TypeScript: API lint down 68%
- Capping fetch depth on change-detection gates: 94s → 20s; removing checkout where unneeded: 27s → 7s
- Scoping pnpm installs to the API package instead of the monorepo: 44–73s → 16–18s
- Loading a schema snapshot instead of replaying migrations per container: ~12s → 1–2s
Two counterintuitive findings are worth more than the techniques. Caching node_modules was slower than rebuilding — 28 seconds to restore a frequently-invalidated cache against 7.5 seconds for a filtered install. And batching seven tiny checks into two jobs running them concurrently saved ~87,000 runner-minutes a month, 11.8% of total CI usage, because each check had been paying full setup for seconds of work.
The sharding arithmetic is the most transferable lesson. Parallelism is usually treated as free, but it’s bounded by per-shard setup: at 110–140 seconds of setup, eight shards would have spent 15–19 minutes on setup alone — more than the tests. Once setup was down to ~40 seconds, eight shards cost less total setup than four had, while parallelizing twice as far.
The largest single win carried the highest correctness risk: an opt-in Vitest project with isolate: false, letting safe files share a module registry instead of rebuilding the entity, GraphQL and decorator graph per file — ~17% of monthly CI spend, slowest shard 300–379s down to ~195s. They made eligibility an explicit per-file opt-in, added teardown for shared state, left fake-timer and untanglable files isolated, and updated their agent skills so generated tests respect the constraint by default. Plainly stated tradeoffs and guardrails, no hand-waving about how it’s probably fine.