06.02.2026

GitHub Actions Performance: 10 Tips to Speed Up Your CI/CD

GitHub Actions Performance

A 10-minute CI pipeline that could run in 3 minutes is costing your team hours every week. Multiply that across dozens of developers and hundreds of commits, and you're looking at serious productivity losses. Here's how to optimize your GitHub Actions workflows for speed.

1. Cache Dependencies Aggressively

The single biggest win for most projects. Instead of downloading dependencies on every run:

- name: Cache node modules
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

- name: Install dependencies
  run: npm ci

Language-specific cache paths:

Language Cache Path
Node.js ~/.npm or node_modules
Python ~/.cache/pip
Go ~/go/pkg/mod
Rust ~/.cargo and target
Java/Maven ~/.m2/repository

Pro tip: Use hashFiles() on lock files to invalidate cache only when dependencies actually change.

2. Use Parallel Jobs with Matrix Strategy

Run tests across multiple versions or configurations simultaneously:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, macos-latest]
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

Set fail-fast: false to continue other jobs even if one fails - useful for seeing the full picture of what's broken.

3. Split Tests into Parallel Shards

For large test suites, split tests across multiple runners:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test -- --shard=${{ matrix.shard }}/4

Many test frameworks support sharding natively (Jest, pytest, RSpec). A test suite that takes 20 minutes can run in 5 minutes with 4 shards.

4. Use Smaller Runner Images

The default ubuntu-latest includes tons of pre-installed software. If you don't need it:

jobs:
  build:
    runs-on: ubuntu-24.04  # Specific version, more predictable
    container:
      image: node:20-slim   # Minimal image with just what you need

Or use a custom Docker image with your dependencies pre-installed.

5. Skip Unnecessary Steps with Conditionals

Don't run deployment on every PR:

- name: Deploy to production
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  run: ./deploy.sh

- name: Run E2E tests
  if: contains(github.event.pull_request.labels.*.name, 'e2e')
  run: npm run test:e2e

Use path filters to skip workflows entirely when irrelevant files change:

on:
  push:
    paths:
      - 'src/**'
      - 'package.json'
      - '.github/workflows/**'
    paths-ignore:
      - '**.md'
      - 'docs/**'

6. Optimize Docker Builds

Docker builds are often the slowest step. Use BuildKit and layer caching:

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: myapp:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

The type=gha cache uses GitHub's cache storage for Docker layers.

7. Use Artifacts for Job Dependencies

Pass build outputs between jobs instead of rebuilding:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - run: ./deploy.sh

8. Consider Self-Hosted Runners

For very frequent builds or specific hardware needs:

jobs:
  build:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - run: make build

Benefits:

  • No queue wait times
  • Persistent caches (no download time)
  • Custom hardware (GPUs, ARM, more RAM)
  • Cost savings at scale

Use actions/runner or managed solutions like Buildjet or Namespace.

9. Avoid Unnecessary Checkouts

The default checkout fetches full history. For most CI jobs:

- uses: actions/checkout@v4
  with:
    fetch-depth: 1  # Shallow clone, just latest commit

For monorepos with sparse checkout:

- uses: actions/checkout@v4
  with:
    sparse-checkout: |
      packages/my-app
      shared/

10. Profile Your Workflows

You can't optimize what you don't measure. Use job summaries and timing:

- name: Build
  run: |
    start=$(date +%s)
    npm run build
    end=$(date +%s)
    echo "Build took $((end-start)) seconds" >> $GITHUB_STEP_SUMMARY

Or use the Actions Timing Insights action for detailed breakdowns.

Quick Wins Checklist

Optimization Typical Savings
Dependency caching 30-60%
Parallel test shards 50-75%
Shallow checkout 5-10%
Path filters 100% (skipped runs)
Docker layer caching 40-70%
Self-hosted runners 20-50% (no queue)

Common Pitfalls to Avoid

  1. Cache key too broad: Use specific keys with lock file hashes
  2. Not using npm ci: It's faster than npm install for CI
  3. Running all tests on every PR: Use path filters and labels
  4. Sequential jobs that could parallel: Review job dependencies
  5. Large artifacts: Compress before upload, clean up after use

Example: Optimized Node.js Workflow

name: CI
on:
  push:
    branches: [main]
    paths-ignore: ['**.md']
  pull_request:
    paths-ignore: ['**.md']

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
          
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
          
      - run: npm ci
      - run: npm run build
      - run: npm test
      
      - uses: actions/upload-artifact@v4
        if: github.ref == 'refs/heads/main'
        with:
          name: build
          path: dist/
          retention-days: 1

FAQ

How much does caching really help?

For a typical Node.js project, caching node_modules can reduce install time from 60+ seconds to under 5 seconds.

Are self-hosted runners worth it?

If you're running 1000+ minutes/month of Actions, self-hosted runners often pay for themselves. Plus, no queue wait times.

Can I cache everything?

Cache storage is limited (10GB per repo). Cache only what's slow to recreate and changes infrequently.


Want to automate CI/CD monitoring and get alerts when builds slow down? Akmatori helps SRE teams track pipeline performance and catch regressions before they impact developer productivity.

Automate incident response and prevent on-call burnout with AI-driven agents!