Can You Cut GitHub Actions Build Times?

Halve a GitHub Actions build by removing queue wait, cold caches, Docker layer rebuilds and setup time, then matching runner cores to parallel work.

Last verified:

Yes. You can cut GitHub Actions build times in half when most of a job's minutes are waiting and setup rather than compute. Queue wait, cold caches, Docker layer rebuilds and environment setup are the minutes to remove. Then match runner cores to the parallel work in the job. Measure first, because the fix that halves one job barely moves another.

WarpBuild provides GitHub Actions runners that cover each of these fixes with label or action swaps in existing YAML.

Can you cut GitHub Actions build time in half?

You can when the job spends at least half its wall clock on minutes that a fix removes. A job that is mostly compute gets its gain from more cores, when its work runs in parallel.

WarpBuild's snapshot runner benchmark (published 2024-09-12) shows how much the result depends on the job. Three open source workflows ran on GitHub-hosted runners and on snapshot runners.

WorkflowGitHub-hosted runnerWarpBuild snapshot runnerResult
Pocketbase build7 to 8 minutes3m19s, including 1m1s of snapshot creation53 to 59 percent less time
Supabase Playwright tests5m25s4m56s, including 1 minute of snapshot creation9 percent less time, including snapshot creation
Flux build and test20m23s14m28s29 percent less time

All three got faster, and the Pocketbase build took less than half the time. How much a job gains depends on how much of its time is setup that a snapshot skips. Your own workflows will vary the same way.

Where the minutes in a slow build go

The minutes in a slow build go to five places, and each has its own fix. Open one representative run and write down two things per job: the gap between the queued and started timestamps, and the duration of each step. Then mark each minute as waiting, setup or compute. The guide to finding the slow step shows how to pull these timestamps from the GitHub API, and the answer on why GitHub Actions builds are slow ranks the causes.

On WarpBuild runners, CI observability shows CPU and memory use per runner instance and lists jobs that could run faster on a different size. That makes CPU saturation visible without adding instrumentation to the workflow.

Queue wait before the job starts

The job sits in the queue for minutes before a single step runs. Machine speed does not matter in this window. The usual causes are account-level limits on parallel jobs, which depend on your plan per GitHub's Actions limits, runs-on labels that no online runner carries, and self-hosted pools sized for average load while demand arrives in bursts. The queue times guide covers how to measure it per runner label.

Cold dependency cache on every run

npm ci, pip install, cargo fetch and go mod download rebuild everything when the cache restore misses. Look at the cache restore step in the log. A miss followed by a multi-minute install step means the job pays the full download and compile cost on every run. When a repository passes its cache storage limit, GitHub saves the new cache and evicts the least recently used entries, per the dependency caching reference. Busy repositories miss even when the workflow is correct. See the GitHub Actions cache size limit, explained.

Environment setup repeated on every run

Some jobs spend minutes installing system packages, toolchains, browsers or databases before the real work starts. Every run starts on a fresh machine, so the same setup runs again each time. If the first steps of a job look the same on every run and take a large share of the wall clock, setup is the target.

Docker layer cache rebuilt each run

Except for single-CPU runners, each GitHub-hosted runner is a new virtual machine, per GitHub's runner reference. So the Docker layer cache from the last build is not on the machine. Every docker build replays every layer, including base image pulls and dependency install layers whose inputs did not change. If the build log shows those layers rebuilding anyway, the layer cache is lost between jobs.

Undersized runner or a serialized matrix

A standard ubuntu-latest runner in a private repository has 2 vCPUs and 8GB of RAM, per GitHub's runner reference. Test runners such as jest --maxWorkers, pytest -n auto and go test -p split work across cores, and on 2 vCPUs that split buys little. The sign is a job with CPU saturated for its whole run while the suite splits cleanly into shards. A matrix has the same problem one level up. Entries that run one after another instead of in parallel point to a workflow-level concurrency group, an old max-parallel setting, or an account-level limit on concurrent jobs set by your GitHub plan.

Which fixes save the most time

The fix that saves the most time is the one aimed at the largest block of minutes in your measurement. The table maps each fix to the minutes it removes.

FixWhat it removesTime saved (example)
Remove queue waitMinutes between queued and startedExample: a job that waits 3 minutes in the queue starts right away
Keep dependency caches warmFull dependency download and install after a cache missExample: a 4 minute npm ci drops to the time it takes to restore the cache
Boot from a snapshot (Ubuntu only)Repeated environment setupExample: 3 minutes of toolchain and package setup are skipped on later runs
Build images on a remote Docker builderRebuilding unchanged Docker layersExample: a 6 minute image build reruns only the layers that changed
Match runner size to parallel workCompute time stuck on too few coresExample: an 8-way test suite on 8 vCPUs instead of 2
Stop serializing matricesMatrix entries waiting for each otherExample: a 12-entry matrix finishes in about the time of its slowest entry

Remove queue wait. Move the job to WarpBuild runners. WarpBuild's generally available Linux and Windows runners have unlimited concurrency, per the cloud runners docs. A 30-entry matrix gets 30 machines instead of waiting for a fixed pool. For high concurrency on macOS, contact [email protected].

Keep dependency caches warm. Replace actions/cache@v6 with WarpBuilds/cache@v2, a drop-in replacement (cache documentation). WarpBuild also maintains cache-enabled versions of popular setup actions, such as WarpBuilds/setup-node, WarpBuilds/setup-python and WarpBuilds/setup-go.

Boot from a snapshot. A snapshot runner saves the state of the runner VM at a point you choose and boots later runs from it. Add snapshot.key=<alias> to the runs-on label and save the snapshot with WarpBuilds/snapshot-save@v1 after setup. Snapshot runners are supported only on WarpBuild Cloud Ubuntu runners. BYOC runners (WarpBuild runners in your own cloud account), Windows runners and macOS runners ignore the labels. A snapshot runner boots in 45 to 60 seconds with setup already done. Snapshots are kept for 15 days. For large directories such as node_modules, the snapshot benchmark post suggests the cache action over files in the snapshot.

Build images on a remote Docker builder. A WarpBuild Docker builder is a dedicated builder VM that keeps its layer cache between builds. Each builder profile is one builder VM. Unchanged layers come from that cache, and you do not need cache-from or cache-to settings. The cache resets if the builder profile is unused for 10 days. The Docker builder docs recommend about 8 vCPU and 16GB of memory per concurrent build job. The remote Docker builders guide covers setup.

Match runner size to parallel work. Pick a label whose vCPU count matches the parallel work in the job. WarpBuild Linux x64 sizes run from 2 vCPUs with 8GB of memory to 32 vCPUs with 128GB, all on a 150GB SSD, per the runner catalog. A suite already sharded eight ways belongs on warp-ubuntu-latest-x64-8x or larger.

Stop serializing matrices. Delete stale max-parallel settings and scope concurrency groups to the git ref instead of the whole workflow. Then let the matrix fan out. The matrix builds guide has the patterns.

What the workflow change looks like

The smallest change is the runs-on label plus the cache action. For a Node.js test job, the diff is:

 jobs:
   test:
-    runs-on: ubuntu-latest
+    runs-on: warp-ubuntu-latest-x64-4x
     steps:
       - uses: actions/checkout@v7
       - uses: actions/setup-node@v7
         with:
           node-version: 22
-      - uses: actions/cache@v6
+      - uses: WarpBuilds/cache@v2
         with:
           path: ~/.npm
           key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-npm-
       - run: npm ci
-      - run: npm test
+      - run: npm test -- --maxWorkers=4

The complete workflow after the change:

name: test
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: warp-ubuntu-latest-x64-4x
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
      - uses: WarpBuilds/cache@v2
        with:
          path: ~/.npm
          key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-
      - run: npm ci
      - run: npm test -- --maxWorkers=4

Four details to know before you roll this out across a repository:

warp-ubuntu-latest-x64-4x resolves to Ubuntu 24.04 with 4 vCPUs, 16GB of memory and a 150GB SSD. The full label list, including the Ubuntu 26.04 and Ubuntu 22.04 variants and the ARM64 labels, is in the runner catalog. To pin the OS version, use the alias warp-ubuntu-2404-x64-4x. It names the same machine without following the latest pointer.

WarpBuilds/cache@v2 keeps the behavior of actions/cache. An exact key match sets cache-hit to true. A restore-keys match restores an older entry and reports false. Cache entries are scoped to key, version and branch. Split restore and save steps and Docker layer caching are in the cache documentation.

The WarpBuild cache is available on Linux runners and is enabled by default. It is not supported on Windows runners, so Windows jobs keep actions/cache and still get WarpBuild's larger machine sizes.

The --maxWorkers=4 flag matters as much as the label. A bigger runner only helps when the job uses the extra cores. Raise the worker count of the test runner, bundler or compiler to match the vCPU count of the label.

Roll the change out in this order:

  1. Measure one representative run and mark each minute as waiting, setup or compute.
  2. Change runs-on to a warp- label to remove queue wait.
  3. Swap actions/cache@v6 for WarpBuilds/cache@v2, or use one of the cache-enabled setup actions.
  4. On Ubuntu jobs with long setup, add snapshot.key=<alias> and a WarpBuilds/snapshot-save@v1 step.
  5. For image builds, switch to WarpBuilds/build-push-action@v7 with a builder profile.
  6. Match the runner size and the worker count to the parallel work in the job.
  7. Measure again. Start with the longest pull request gate and leave release workflows for last. To roll back, change the label back.

What a faster build costs

A faster build costs about the same per job only when the wall clock drops as fast as the price per minute rises. WarpBuild's per-minute price doubles each time the vCPU count doubles. So if doubling the cores halves the wall clock, the bill stays flat. If the build does not speed up enough to offset the higher rate, the bill goes up, and this is true on GitHub's larger runners too. So measure before you size up. At every size in the table below, the WarpBuild rate is lower than GitHub's.

GitHub prices below come from the GitHub Actions minute multipliers reference. They apply to private repositories. Standard runners in public repositories are free, per GitHub Actions billing. WarpBuild prices come from the runner catalog and the pricing page.

vCPUGitHub-hosted Linux x64, per minuteWarpBuild Linux x64, per minuteLabel
2$0.006 (standard)$0.004warp-ubuntu-latest-x64-2x
4$0.012 (larger runner)$0.008warp-ubuntu-latest-x64-4x
8$0.022 (larger runner)$0.016warp-ubuntu-latest-x64-8x
16$0.042 (larger runner)$0.032warp-ubuntu-latest-x64-16x
32$0.082 (larger runner)$0.064warp-ubuntu-latest-x64-32x

Example, with assumptions you can replace with your own numbers: 6,000 jobs per month, 12 minutes each on ubuntu-latest, in a private repository.

  • Today. 6,000 x 12 = 72,000 minutes. At $0.006 per minute, that is $432 per month.
  • Same speed on WarpBuild. On warp-ubuntu-latest-x64-2x at $0.004, the same 72,000 minutes cost $288 per month.
  • Halved on WarpBuild. Assume the job moves to warp-ubuntu-latest-x64-4x and drops from 12 minutes to 6, because the suite now runs on twice the cores. That is 36,000 minutes at $0.008, or $288 per month, a third less than today. Each run returns 6 minutes to the person waiting on it, which is 600 hours per month across the team.
  • Halved on GitHub's 4-core larger runner. The same 36,000 minutes at $0.012 cost $432 per month, the same as today.
  • Halved without a bigger runner. If the time saved comes from queue wait, caches or setup instead, the job can stay on warp-ubuntu-latest-x64-2x, and 36,000 minutes cost $144 per month.

When the bill matters more than speed, How to Cut Your GitHub Actions Bill Without Slowing Builds covers right-sizing for cost.

FAQ

Is a 2x faster build realistic for every workflow?

No. It is realistic when most of the job is waiting and setup, such as queue wait, cold caches, Docker layer rebuilds and environment setup. A job that spends most of its time in single-threaded compute gets smaller gains. In WarpBuild's snapshot benchmark (https://www.warpbuild.com/blog/snapshot-runners), one workflow took less than half the time, and the other two took 9 and 29 percent less time.

Do I need to rewrite my workflows to make them faster?

No. Most fixes are a change to the runs-on label or a swap of one action for a compatible one, such as WarpBuilds/cache@v2 for actions/cache@v6. Build steps stay the same.

Which fix should I try first?

The one that removes the most minutes in your own measurement. Queue wait is usually the cheapest to check, because a faster machine does not help a job that has not started yet.

Do snapshot runners work on macOS or Windows?

No. Snapshot runners are supported only on WarpBuild Cloud Ubuntu runners. Snapshot labels on other runner types are ignored and the job runs normally.

Does the WarpBuild cache work on Windows runners?

No. WarpBuild caching is not supported on Windows runners. Windows jobs keep actions/cache and still get WarpBuild's larger machine sizes.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.