DEV Community

jamilxt
jamilxt

Posted on

AI Coding Agents Made CI the Bottleneck. Here Is the Fix, Step by Step.

Your AI agent finishes a pull request in four minutes. Then it sits in your CI queue for eleven. If you have agents opening PRs at 2am, on weekends, and in batches of five, you already know the feeling: the agent was not the slow part of the loop. The pipeline is.

Linear published a write-up of this exact problem yesterday, and it hit the Hacker News front page within hours with more than 250 points. The title says it plainly: "AI coding has made CI a bottleneck, so we reworked ours to keep up." Their situation will sound familiar. Agents made shipping code exponentially faster, but every PR still passes through CI, so the test suite grew almost 4x since January while the pipeline stayed the same.

What makes the post worth your time is the outcome. Linear brought pull request wait time down from more than 6 minutes to just over 5, cut runner time per test roughly in half, and kept it there while adding roughly 2,000 tests per week. Without the rework, today's suite would take about 11 minutes to validate.

This article is my breakdown of that post, translated into concrete GitHub Actions changes. I have not run Linear's internal pipeline, and their stack is TypeScript on pnpm, so treat every number below as theirs. But the four levers they pulled are not Linear-specific. They work on almost any pipeline where the slow part is not the tests themselves.

First, find out where the time actually goes

Before touching anything, do what Linear did: measure the wait and the runner time separately. They are different problems.

  • Wait time is how long a PR sits before CI says yes or no. Developers and agents feel this directly.
  • Runner time is how much compute the pipeline burns. Your invoice feels this directly.

Linear optimized both, and the surprising part of their breakdown is how little of either came from the actual test execution. Most of it was setup: checkout, dependency install, container boot, and small "gate" jobs that everything else waits on.

Pull your last 50 CI runs and bucket the time of your slowest workflow into four stages: checkout and fetch, dependency setup, gating jobs, and test execution. Whatever is not test execution is where the money hides.

Lever 1: gate jobs, the tiny jobs that block everything

Every pipeline has them: a change-detection job that figures out which paths a PR touched, or a cache check that decides what to skip. They look harmless. They are not. If eight test shards cannot start until one 26-second gate finishes, that gate is on the critical path eight times over.

Linear's change-detection jobs were checking out the full working tree just to run a diff. The fix was capping the fetch depth, which took the slowest gate from 94 seconds to 20. Jobs that never needed a working tree dropped from 27 seconds to 7 once checkout was removed entirely. The median gate fell from 26 to 8 seconds.

On GitHub Actions, that looks like this:

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2        # diff needs two commits, not the history
          sparse-checkout: .    # no full tree if you only read the diff
Enter fullscreen mode Exit fullscreen mode
  • Cap the fetch depth. Most detection jobs only need the commits in the PR, not the repository's whole history.
  • Drop checkout where you can. If a job only reads an environment variable or an artifact, do not clone at all. Seven seconds saved on a job that gates eight shards is closer to a minute saved end to end.

One more from Linear: they were writing a cache marker as part of the final merge check, which meant PRs sat in the merge queue after tests already passed. Moving that write into a job that runs after the shards but gates nothing shaved 42 seconds off the merge path for every API PR. Look for writes and housekeeping that snuck onto your critical path. They are usually there.

Lever 2: kill the repeated setup tax

A job that does ten seconds of useful work can burn three minutes of runner time on boot, install, and provisioning. Linear attacked this from three directions, and the combined result cut per-shard setup by roughly 44%, from 110 to 140 seconds down to 67 to 73 seconds.

  • Preinstall shared dependencies in the CI image. Every test shard was spending 7 to 8 seconds installing the same Postgres client with apt on every run. Baking it into a small base image made that cost zero. If you use a self-hosted runner or a Docker-based job, put your stable dependencies in the image and leave only fast-moving ones to the install step.
  • Install only what the job needs. Their API workflow installed the entire pnpm monorepo when it needed one package. A filtered install cut it from 44 to 73 seconds down to 16 to 18 seconds. In GitHub Actions terms, if you run tests for one service in a monorepo, install that service's dependency subtree, not the workspace.
  • Do not cache when rebuilding is faster. This one is counterintuitive. Linear tried caching node_modules and found a cache hit cost about 28 seconds to restore, versus roughly 7.5 seconds for a filtered install, because the cache key rode a frequently changing lockfile. Cache is not free. Measure restore time plus save time against rebuild time before you keep it.

They found the same pattern one level down: API containers replayed the full database migration history on every run even when the PR did not touch the schema. Loading a generated schema snapshot instead cut database setup from about 12 seconds to 1 to 2 seconds per container. If your tests boot a database per job, ask whether a snapshot would do.

Lever 3: batch the short checks

Seven independent checks, each paying full runner boot plus checkout plus install, for a few seconds of work each. Linear consolidated the seven into two jobs that ran the seven tasks concurrently inside them. That change alone saved roughly 87,000 runner-minutes per month, about 11.8% of their total CI usage, based on June numbers.

The GitHub Actions shape of this is a job matrix where each entry runs several fast scripts instead of one:

jobs:
  fast-checks:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        group: [static, hygiene]
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 1 }
      - run: |
          case "${{ matrix.group }}" in
            static)  npm run lint && npm run typecheck ;;
            hygiene) npm run format:check && npm run audit:deps ;;
          esac
Enter fullscreen mode Exit fullscreen mode

Group by cost, not by concern. Two groups each paying setup once beats seven groups each paying setup seven times, as long as a failure in one group does not hide failures in others. Keep the reporting per-task so a red check still points at the exact script.

Lever 4: shard the tests, but only after setup is cheap

This is the step most teams do backwards. They add shards, the queue gets shorter, the bill gets bigger, and nothing converges. Linear's numbers explain why.

  • Before setup cuts: 4 shards spent 8.3 minutes on setup. Doubling to 8 shards would have spent 15 to 19 minutes of runner time on setup alone, more than the tests themselves.
  • After setup cuts: setup is around 40 seconds, so 8 shards now spend less total setup time than 4 did before, while running the tests twice as wide.

With cheap setup in place, going from 4 to 8 shards made their critical test job roughly 19% faster and 19% cheaper. One subtlety: Vitest balances work by file, not by duration, so a few giant test files were pinning entire shards. They split the large files so the scheduler could actually balance. If your test runner distributes by file, your shard wall time is decided by your biggest file, so shave it first.

Their largest single win, worth roughly 17% of monthly spend, came from letting safe test files share a module registry (isolate: false) instead of rebuilding the full graph per file. Slowest shard fell from roughly 300 to 379 seconds to about 195 seconds. But they flag it as the highest correctness risk, and they left any file with fake timers or shared state in an isolated project. Treat this as an opt-in per file, not a global flag you flip on a Friday.

The counterintuitive bits worth remembering

Some of Linear's findings go against cache-everything folklore, and they are the most transferable lessons in the post.

  • Faster hardware was the cheapest win. Moving off GitHub Actions to third-party runners made jobs 34% faster on a like-for-like comparison of the two days around the switch, with typechecking dropping 52%. Sometimes the fix is buying faster machines, not optimizing anything.
  • A cache miss can beat a cache hit. 28 seconds to restore node_modules versus 7.5 seconds to reinstall a filtered subset. Cache keys that ride hot lockfiles turn caching into a slow path with extra steps.
  • Compiler choice moved the bottleneck entirely. Switching to the native TypeScript compiler cut the weekly median of their typecheck by 73%, large enough that typechecking stopped being a bottleneck at all. The equivalent question in your stack: which single check, if it got 3x faster, would stop being the thing everyone waits on?

One more thing: teach your agents the constraints

Here is the detail I found most interesting. Because agents now write the majority of Linear's tests, they updated their agent skills so generated tests follow the same constraints as the optimized setup, like the shared-module-state opt-in. Otherwise the agents would happily generate tests that quietly disqualify themselves from the fast path.

If you are using coding agents, your CI conventions are part of their context. Write them down where the agent reads them: which files can share state, which checks are batched, what belongs in the base image. A pipeline you tune by hand can be un-tuned by an agent that does not know the rules.

The checklist

Save this. It is the order I would work through on any pipeline where agents are outgrowing CI:

  • Measure first. Bucket your slowest workflow into fetch, setup, gating, and test time. Optimize the largest non-test bucket.
  • Fix gate jobs. Shallow fetch, no checkout where possible, move cache writes off the critical path.
  • Cut setup tax. Stable deps in the image, filtered installs per job, snapshot instead of replay for databases.
  • Batch short checks. Fewer jobs, more tasks per job, per-task reporting preserved.
  • Then shard. Only after setup is cheap. Split oversized test files so balancing actually works.
  • Re-measure. Cache decisions and shard counts are not permanent. What was faster last quarter may be slower now.

Linear ended up roughly a minute faster on the required check for API PRs on cache misses, with tests 4x larger than in January. None of the individual tricks are exotic. The compounding is what does the work: a second here and eight seconds there, on the critical path, times every PR your agents file.

Do you know what your agents are waiting on? Pull your last 50 CI runs tonight and find out. The answer is rarely the tests.


I write about developer tools, AI infrastructure, and backend engineering every week. Subscribe, it is free, and it tells me this kind of deep dive is worth doing.

Sources: Linear, "AI coding has made CI a bottleneck, so we reworked ours to keep up" by Mufeez Amjad, September 21, 2026 (linear.app/now/ci-bottleneck-reworked). All performance figures in this article are Linear's reported numbers, not independent measurements.

Top comments (0)