Skip to content
Sengi

How to speed up GitHub Actions: find and fix slow CI

How do I speed up GitHub Actions?

Speed up GitHub Actions by measuring the pull request's critical path, then removing its largest proven delay: queue time, repeated setup, cache misses, duplicate builds, serial job dependencies or a CPU- and memory-bound runner. Optimize one measured phase at a time and compare representative runs with the same cache state. A larger runner helps only when the job can use the extra CPU, memory or disk; it does little for network waits, approvals or serial commands.

Last checked September 21, 2026: the workflow monitoring, caching, job dependency, matrix and runner specification sources linked below were checked against GitHub's current documentation on that date. The Sengi numbers in the last section were measured October 5, 6 and 9, 2026.

What should I measure before changing the workflow?

Measure the time from the workflow being ready to run until the last required check finishes. Developers feel that wall-clock delay, not the sum of every job's minutes. Open a representative pull request run and use GitHub's visualization graph to identify the chain of jobs that determines completion. Within each job, copy the duration of setup, checkout, dependency installation, build and tests from the log.

Keep queue time separate from execution. A faster command cannot shorten a runner queue, and extra capacity cannot shorten a serial test that already has the machine. Record cold-cache and warm-cache runs separately; averaging them together can make a cache that misses most of the time look useful. GitHub's workflow monitoring guide, checked September 21, 2026, points to the graph, job and step status, logs and job execution time as the built-in evidence.

PhaseIllustrative timeQuestion to answer
Runner queue0:45Was an eligible runner available?
Set up job and checkout0:35Is the image, container or checkout doing repeated work?
Install dependencies2:40Did the intended cache restore, and was it cheaper than installing?
Build3:30Is the same output built in another job?
Tests5:00Can the suite safely split, and is it CPU or memory bound?
Total critical path12:30Which single change can remove the largest proven delay?

This is illustrative arithmetic. The five phase rows add to the 12-minute-30-second total; they are not a benchmark or a promised result. In this example, cutting ten seconds from checkout cannot matter as much as fixing the five-minute test phase. Substitute one of your own runs before choosing the work.

How do I shorten the critical path?

Remove dependencies that express convenience rather than a real data requirement. GitHub runs jobs in parallel by default; needs makes them sequential. Lint, unit tests and a build can usually start together if each checks out the repository and none consumes another's output. A release can wait for all three.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: ./ci/lint

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: ./ci/test

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: ./ci/build

  package:
    needs: [lint, test, build]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: ./ci/package

Suppose lint takes 2 minutes, tests 6 and build 4. Run serially, those three phases take 12 minutes before packaging; in parallel, they take 6, the longest branch. That six-minute difference is illustrative arithmetic and excludes queue and setup overhead. The actual gain is the old critical path minus the new one, which is why the graph matters more than the number of jobs.

Do not remove a real dependency. If tests consume the exact binary produced by build, keep the edge and transfer that binary as an artifact rather than silently rebuilding it. GitHub documents the default parallel behavior and needs semantics in its workflow syntax reference.

How do I tell whether caching makes GitHub Actions faster?

Compare the restore, install and save durations on a known cache hit and a known miss. A useful cache has a stable key tied to the files that change the dependency set, restores the directory the package manager actually uses, and saves more install time than restore and save consume. The log must show a hit; the presence of a cache step does not prove one.

steps:
  - uses: actions/checkout@v7
  - uses: actions/setup-node@v7
    with:
      node-version: 24
      cache: npm
      cache-dependency-path: package-lock.json
  - run: npm ci

This current setup-node pattern caches npm's global package data and keys it from the lockfile; it does not cache node_modules. The lockfile should be committed, and a monorepo should point cache-dependency-path at the lockfile or lockfiles that belong to the job. GitHub's dependency caching reference, checked September 21, 2026, defines exact hits, partial restore-key matches and cache creation after a successful miss. The official setup-node documentation documents the package-manager cache behavior and current syntax.

A cache can make a workflow slower when it changes on every commit, restores a large directory over the network, or duplicates a package manager's own efficient download. Keep the cache only if the measured warm path wins and the miss path remains acceptable. Never cache credentials or tokens; GitHub warns that pull requests can access caches available to their base branch.

Where does duplicate CI work hide?

Search every job and called workflow for the expensive commands in your timing budget. Common duplicates are installing the same dependency set twice in one job, compiling the same commit separately for tests and packaging, rebuilding a container that a previous job already produced, and running an unchanged test suite in both a caller and reusable workflow.

Choose one owner for each output. If several jobs need an identical build result, build it once and pass an immutable artifact identified by the commit. Measure upload and download time as part of the change: a 4 GB artifact sent across three jobs can cost more wall time than rebuilding a small binary. If jobs only share downloaded dependencies, a cache is usually a better fit than an artifact because the output is reusable across workflow runs.

Also inspect matrices. Every matrix combination is a separate job, and a Cartesian product can repeat setup and compilation across versions that do not need them. Keep the combinations that express support or catch distinct failures; move lower-value breadth to a scheduled or pre-release workflow only when that matches the project's test policy.

When should I split tests into parallel jobs?

Split tests when one test phase dominates the critical path, the tests are independent, and runners are available for every shard. Balance shards by measured duration rather than file count, because ten fast files and one slow integration file are not two equal halves. Preserve failure logs and merge coverage or reports explicitly.

jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: ./ci/test-shard "${SHARD}" 3
        env:
          SHARD: ${{ matrix.shard }}

The ./ci/test-shard command is an adapter your repository must provide for its test runner; the workflow syntax itself is valid. If a 9-minute suite splits into three perfectly balanced 3-minute shards, the test branch becomes 3 minutes plus repeated setup. That is illustrative arithmetic, not a measured speedup. Real shards are uneven, each pays checkout and installation overhead, and limited runner capacity can put them back in a queue.

strategy.max-parallel limits how many matrix jobs run at once. It protects scarce capacity but cannot make a six-job matrix finish faster than allowing all six to run when six runners are available. GitHub's matrix guide, checked September 21, 2026, documents the setting and the fact that it limits simultaneous jobs.

Will a larger runner make GitHub Actions faster?

Only if the slow phase can use what the larger machine adds. Sustained CPU saturation, memory pressure, swapping or a disk-bound build are reasons to test another size. Time spent downloading dependencies, waiting for a database to become ready, calling an external API or running a serial command usually changes little with more cores.

Observed bottleneckUseful experimentWhat would confirm it
All cores busy during build or testsTry the next CPU size with the same commit and cache stateThe critical step, not only setup, falls materially
Memory exhaustion or heavy swapTry more memoryFailures stop or the pressured phase shortens
Long runner queueAdd eligible capacity or reduce fan-outQueue time falls; command time may stay the same
Network or external-service waitFix locality, retries or service readinessWait time falls without a size change
Mostly serial executionProfile and parallelize the command firstThe command begins using added cores

Compare representative runs, including the same cache state, and keep the smaller runner if the larger one only moves noise. GitHub's hosted runner reference, checked September 21, 2026, lists the CPU, memory, storage and architecture behind each standard label. Public and private repositories can receive different hardware from the same label, so record repository visibility as part of the comparison.

What order should I optimize GitHub Actions in?

The reliable route to faster GitHub Actions is to follow the evidence from the largest delay, rather than stacking generic optimizations. Use this sequence:

  1. Fix jobs that are blocked or repeatedly retrying; the stuck-job diagnostic guide separates runner queues from workflow gates and hung steps.
  2. Measure the required-check critical path and make a timing budget from a representative run.
  3. Remove duplicate work and accidental needs dependencies.
  4. Fix the largest cache miss only after proving the intended key and path.
  5. Split a dominant test phase when shards can be balanced and capacity exists.
  6. Right-size the runner after the workflow is doing the right work.

This order prevents a larger machine from hiding an avoidable install or duplicate build. It also keeps cost visible: more parallel jobs can shorten wall-clock time while increasing total job-minutes. The GitHub Actions cost checklist covers that second dimension, the pricing guide explains GitHub's meters, and the cost calculator applies the published rates to your own minutes.

Where can Sengi help?

When the critical path is CPU-bound, the fastest fix is a faster CPU. Sengi runs your Linux jobs on bare-metal desktop CPUs boosting past 5 GHz, at 2, 4 or 8 vCPU, with a fresh VM for every job and up to 74% less per build than GitHub. 2 vCPU runners are pre-booted: your job lands on a VM that’s already running, ready in 2 seconds. Your workflow, required checks and logs stay in GitHub. Change runs-on by hand, or give your coding agent the Sengi prompt and it opens the pull request for you.

The measured numbers. On the same 2 vCPU as GitHub’s ubuntu-latest, a Sengi runner on its AMD Ryzen 9 7950X3D delivers up to 2.75× the multi-core and 1.6× the single-core CPU throughput. On real builds that means up to 1.8× faster. We ran a Node + Docker app, Vite, Mastodon, a Rust and a Go project’s CI on both. Same 2 vCPU:

Job, same 2 vCPUGitHubSengiFaster
Node + Docker build539 s299 s1.8×
Vite build and test552 s424 s1.3×
Mastodon Docker image build252 s215 s1.17×
fd (Rust) build and test76 s67 s1.13×
GitHub CLI (Go) test and build367 s329 s1.12×

Medians, October 2026. The Node + Docker build’s CPU-heavy step ran 2.7× faster, and the whole job cost $0.015 a run against $0.054 on GitHub. The other four jobs cost 50–60% less per run than on GitHub, and Docker builds run 1.9× faster with layer caching, included free.

On RunsOn’s open benchmark harness, run on Sengi October 9, 2026 at 4 vCPU against RunsOn’s published GitHub results, Sengi built a production Docker image 1.9× faster than GitHub at 74% less per build (7m34s against 14m36s; $0.045 against $0.175), and ran the Rust, TypeScript and Docker builds together 1.8× faster at 72% less. Disk-bound steps get 15× the random-read disk speed of GitHub’s runner (296k against 19.6k IOPS) and 3× the random writes.

A faster runner stacks with every fix above. See how switching works and runner pricing, or the self-hosted runner cost guide if you are weighing running your own fleet.

Sources checked for this guide

  • GitHub: Monitor workflows — visualization graph, logs and execution time; checked September 21, 2026.
  • GitHub: Workflow syntax — default job parallelism, needs and matrix behavior; checked September 21, 2026.
  • GitHub: Dependency caching reference — hit, miss, key, path and security behavior; checked September 21, 2026.
  • GitHub's actions/setup-node documentation — npm cache scope and lockfile configuration; checked September 21, 2026.
  • GitHub's actions/checkout documentation — current actions/checkout@v7 syntax used before repository-local commands; checked September 21, 2026.
  • GitHub: Running variations of jobs — matrix fan-out and max-parallel; checked September 21, 2026.
  • GitHub: Hosted runners reference — current standard-runner resources and labels; checked September 21, 2026.

Cut your GitHub Actions bill by up to 74%.

Up to half GitHub’s price per minute, on 5 GHz bare metal. Builds finish up to 1.9× faster than on GitHub’s runners, so together you pay up to 74% less per build. Change one runs-on line, or give your coding agent one prompt and it opens the pull request for you.

Start free

3,000 free minutes a month, no card. See how switching works