A slow CI pipeline changes how a team works. People batch changes, skip waiting for checks and context-switch while builds run. With AI agents opening more pull requests than ever, CI speed matters even more.

Here are the changes that usually make the biggest difference, in rough order of payoff.

1. Cache dependencies

Installing dependencies from scratch on every run is the most common waste. The setup-* actions have built-in caching:

yaml
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: pnpm
- run: pnpm install --frozen-lockfile

The cache key is based on your lockfile, so it is invalidated only when dependencies change.

2. Cache build outputs

Tools like Turborepo, Nx, Gradle and Bazel can cache task outputs. Combined with a remote or Actions cache, unchanged packages are not rebuilt or retested.

3. Run jobs in parallel

Split independent work into separate jobs that run at the same time:

yaml
jobs:
  lint:      { runs-on: ubuntu-latest, steps: [ ... ] }
  typecheck: { runs-on: ubuntu-latest, steps: [ ... ] }
  test:      { runs-on: ubuntu-latest, steps: [ ... ] }

4. Shard slow test suites

Use a matrix to split tests across runners:

yaml
test:
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - run: npx vitest run --shard=${{ matrix.shard }}/4

5. Only run what changed

  • Use paths filters so documentation changes do not run the full test suite.
  • In monorepos, run tasks only for affected packages.
yaml
on:
  pull_request:
    paths: ["src/**", "package.json", "pnpm-lock.yaml"]

6. Cancel outdated runs

When you push again, the previous run is usually obsolete:

yaml
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

7. Speed up Docker builds

Use BuildKit layer caching with the GitHub Actions cache backend:

yaml
- uses: docker/build-push-action@v6
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

8. Use bigger runners where it counts

For CPU-heavy builds and tests, larger runners can cost less overall because they finish faster. Measure before and after.

Keep it secure while you optimise

  • Pin third-party actions to a commit SHA.
  • Set minimal permissions: for the GITHUB_TOKEN.
  • Be careful with caches and pull_request_target on public repositories, which can expose secrets to untrusted code.

Key takeaways

  • Cache dependencies and build outputs first.
  • Parallelise jobs and shard slow test suites.
  • Skip unaffected work and cancel superseded runs.
  • Optimise without loosening permissions or pinning.