How to Speed Up GitHub Actions Builds
Faster GitHub Actions builds come from more vCPUs, warm caches, and shorter queues. Swap ubuntu-latest for a WarpBuild warp- label and see the cost math.
Last verified:
Most GitHub Actions builds get faster through three changes: give each job enough vCPUs, keep dependency and Docker layer caches warm between runs, and remove the queue wait before the job starts. WarpBuild covers all three with a one-line edit: a warp-ubuntu-latest-x64-8x label in runs-on replaces ubuntu-latest, and WarpBuilds/cache@v1 replaces actions/cache@v4 wherever a workflow caches dependencies.
This guide diagnoses the five failure modes behind most slow workflows, maps each one to its fix, shows the exact YAML, and works a monthly cost model from GitHub list prices and WarpBuild per-minute rates.
Diagnosis
Slow is a symptom with several distinct causes, and the fixes differ. Before changing anything, open one representative workflow run and collect two numbers per job: the gap between the queued timestamp and the started timestamp, and the duration of each step. Five patterns account for most of the lost wall clock.
GitHub's run page gives coarse timings. For per-step resource detail on WarpBuild runners, CI observability correlates system metrics from the runner agent with the GitHub Actions job log, which makes CPU saturation and cache download stalls visible without adding any instrumentation to the workflow.
Queue wait before the job starts
The job shows minutes of queue time before a single step executes. Machine speed never enters the picture during this window; the run is waiting for a runner to become available. The usual causes are account-level ceilings on parallel jobs, runs-on labels that no online runner carries, and self-hosted pools sized for average load while the actual demand arrives in bursts. Measuring queue wait per runner label and fixing each cause has its own guide: GitHub Actions queue times and how to fix them.
Cold dependency cache on every run
npm ci, pip install, cargo fetch, and go mod download each rebuild everything when the cache restore misses. Look at the cache restore step in the log: a reported miss followed by a multi-minute install step means the job pays the full download and compile cost on every run. GitHub evicts cache entries once a repository passes its storage ceiling, so busy repositories cycle through misses even when the workflow is configured correctly. The eviction mechanics and the fixes are covered in the GitHub Actions cache size limit, explained.
Undersized runner for a parallel test suite
A standard ubuntu-latest job runs on 2 vCPUs. 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 signature is a job whose CPU sits saturated for its whole duration while the suite itself splits cleanly into independent shards. The WarpBuild runner catalog lists Linux x64 sizes from 2 to 32 vCPUs, so the runner can match the parallelism the suite already has.
Docker layer cache rebuilt each run
GitHub-hosted runners start with an empty Docker layer cache because each job receives a fresh machine. Every docker build replays every layer, including base image pulls and dependency install layers whose inputs have identical hashes run after run. If the build log shows those layers rebuilding anyway, the layer cache is being discarded between jobs. Persistent layer caching options are covered in Docker builds on GitHub Actions.
Single-job serialization of an otherwise parallel matrix
A 12-entry matrix should finish in roughly the time of its slowest entry. When entries run one after another instead, look for three culprits: a workflow-level concurrency group that queues siblings behind each other, a max-parallel setting left over from an older constraint, and an account-level ceiling on concurrent jobs. Per-label wait data confirms which one applies; the measurement walkthrough is in the queue times guide.
Fix
Each failure mode above has a direct fix, and four of the five are label or action swaps inside existing YAML.
Queue wait. Move the job to WarpBuild runners. Run as many jobs as your workflows need. Generally available Linux and Windows runners do not have plan-level concurrency caps. Capacity adjusts dynamically, so a 30-entry matrix claims 30 machines instead of queueing behind a fixed pool.
Cold dependency caches. Replace actions/cache@v4 with WarpBuilds/cache@v1. The action is a drop-in replacement: same path, key, and restore-keys inputs, same cache-hit output. WarpBuild also maintains cache-enabled forks of the popular setup actions, including WarpBuilds/setup-node, WarpBuilds/setup-python, and WarpBuilds/setup-go, which route toolchain and dependency caching through the same store with zero other workflow edits.
Undersized runners. Pick a label whose vCPU count matches the parallelism of the job. On Linux x64 the sizes run from 2 vCPUs with 8GB of memory up to 32 vCPUs with 128GB, all on a 150GB SSD. A suite already sharded eight ways belongs on warp-ubuntu-latest-x64-8x or larger.
Docker layers. For workflows that build images, WarpBuild's remote Docker builders keep a persistent layer cache on dedicated build machines, so unchanged layers stop rebuilding. Setup and tradeoffs are on the Docker builds solution page.
Serialized matrices. Delete stale max-parallel settings, scope concurrency groups to the git ref instead of the whole workflow, and let the matrix fan out. With no hard concurrency caps in the runner pool, the matrix completes in roughly the wall clock of its slowest entry.
None of these changes touch build logic. The workflow files change by a handful of lines. The per-size rates behind that comparison are on the pricing page and in the cost model below.
Configuration
The minimal change is the runs-on label plus the cache action. Applied to a Node.js test job, the diff is:
jobs:
test:
- runs-on: ubuntu-latest
+ runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- - uses: actions/cache@v4
+ - uses: WarpBuilds/cache@v1
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=8The complete workflow after the change:
name: test
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: WarpBuilds/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm ci
- run: npm test -- --maxWorkers=8Four details worth knowing before rolling this out across a repository:
warp-ubuntu-latest-x64-8x resolves to Ubuntu 24.04 with 8 vCPUs, 32GB 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 an OS version explicitly, use the alias form: warp-ubuntu-2404-x64-8x names the same machine without following the latest pointer.
WarpBuilds/cache@v1 keeps the semantics of actions/cache@v4: an exact key match sets cache-hit to true, a restore-keys match restores a stale entry and reports false, and cache entries are scoped to key, version, and branch. Advanced patterns, including split restore and save steps and Docker layer caching through the cache proxy, are in the cache documentation.
The cache is available on Linux runners and is enabled by default. WarpBuild caches are unsupported on Windows-based runners, so Windows jobs keep actions/cache while still gaining the larger machine sizes.
The --maxWorkers=8 flag matters as much as the label. A bigger runner only pays off when the job uses the extra cores, so raise the worker count of the test runner, the bundler, or the compiler to match the vCPU count of the label you chose.
Roll the change out one workflow at a time. Start with the longest pull request gate, since that is the wait engineers feel most, and leave release workflows for last. Because the label is the routing mechanism, a rollback is the same one-line change in the other direction, and both versions of the workflow keep running against the same repository while you compare them.
Cost or Time Model
GitHub's per-minute list prices below come from the GitHub Actions minute multipliers reference and the GitHub-hosted runner specifications, checked on 2026-08-13. They apply to private repositories, where minutes are billed. WarpBuild rates come from the WarpBuild pricing page.
| vCPU | GitHub-hosted Linux, per minute | WarpBuild Linux x64, per minute | WarpBuild label |
|---|---|---|---|
| 2 | $0.006 (ubuntu-latest) | $0.004 | warp-ubuntu-latest-x64-2x |
| 4 | $0.012 (larger runner) | $0.008 | warp-ubuntu-latest-x64-4x |
| 8 | $0.022 (larger runner) | $0.016 | warp-ubuntu-latest-x64-8x |
| 16 | $0.042 (larger runner) | $0.032 | warp-ubuntu-latest-x64-16x |
| 32 | $0.082 (larger runner) | $0.064 | warp-ubuntu-latest-x64-32x |
The table pairs each GitHub-hosted size with the WarpBuild label at the same vCPU count. Against those GitHub list prices the WarpBuild rate is 33 percent lower at 2 and 4 vCPU, 27 percent lower at 8, 24 percent lower at 16, and 22 percent lower at 32, all checked on 2026-08-13. The worked example below applies both rates to a realistic monthly volume so the difference shows up in dollars.
Assumptions, stated so the arithmetic can be rerun with your own numbers:
- 6,000 jobs per month, about 300 jobs per working day
- 12 minutes of billed wall clock per job today on
ubuntu-latest(2 vCPUs) - a private repository, so every minute is billed
- GitHub list prices as checked on 2026-08-13
Baseline. 6,000 jobs x 12 minutes = 72,000 minutes per month. At $0.006 per minute on ubuntu-latest, the monthly bill is $432.
Same size, new runner. On warp-ubuntu-latest-x64-2x at $0.004 per minute, the same 72,000 minutes cost $288 per month. Wall clock is assumed unchanged in this row; the only change is the per-minute rate.
Right-sized runner. Suppose the job is a parallel test suite and moves to warp-ubuntu-latest-x64-8x at $0.016 per minute. Assume wall clock drops from 12 minutes to 5 as the suite spreads across 8 vCPUs. Treat that as a planning assumption and verify it against your own suite. The month becomes 6,000 x 5 = 30,000 minutes, costing $480. Beyond the bill, each run returns 7 minutes to whoever is waiting on it: 42,000 engineer-minutes per month, or 700 hours across the team.
The same speed on GitHub's larger runner. The identical 30,000 minutes on GitHub's 8-core larger runner at $0.022 per minute would cost $660 per month.
If the bill is the primary concern and speed is secondary, cutting GitHub Actions costs without rewrites covers right-sizing with per-job billed time reports and runner label breakdowns.
FAQ
Do I need to rewrite my workflows to use WarpBuild runners?
No. Change the runs-on label to a warp- label such as warp-ubuntu-latest-x64-8x. Steps, actions, and secrets keep working because the runner images carry the same tooling as GitHub-hosted runners.
Does WarpBuild limit how many jobs can run at once?
Run as many jobs as your workflows need. Generally available Linux and Windows runners do not have plan-level concurrency caps. Capacity adjusts dynamically with your workload.
What do WarpBuild Linux runners cost per minute?
Linux x64 runners range from $0.004 per minute for 2 vCPUs to $0.064 per minute for 32 vCPUs.
Does the WarpBuild cache work on Windows runners?
No. WarpBuild caches are unsupported on Windows-based runners. The cache is available on Linux runners and is enabled by default, and Windows jobs can keep using actions/cache.
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.