How to speed up GitHub Actions: 6 fixes, in order
On this page
GitHub Actions is slow for two reasons: every job starts on "a new virtual machine" with no caches, and ubuntu-latest has 2 CPUs on a private repository (GitHub Docs, read November 11, 2026). Fix it in order: read step timings, cache dependencies and Docker layers, run in parallel, then add cores or keep the disk.
Fluxenic, the company behind Convika, makes Skipbox, a GitHub Actions runner that keeps its disk between jobs, so one fix on this page is our own product. The GitHub, Docker, and Playwright facts below come from their own docs, read November 11, 2026 and linked under Sources.
The six fixes, and what each one removes
| Order | Fix | What it removes | What it costs |
|---|---|---|---|
| 1 | Read the step timings | Guessing | Nothing |
| 2 | Cache dependencies | Registry downloads on a cache hit | A cache restore and save in every job. GitHub keeps 10 GB a repository and removes entries that "have not been accessed in over 7 days" |
| 3 | Cache Docker layers | Rebuilding unchanged layers | A cache export and import over the network, inside the same 10 GB |
| 4 | Run in parallel and shard | Waiting for one long job | More billed minutes: every job repeats its setup and rounds up to a whole minute |
| 5 | A larger GitHub runner | Slow CPU-bound steps | A higher rate: $0.012 a minute for 4-core, $0.022 for 8-core, $0.042 for 16-core |
| 6 | A runner that keeps its disk between jobs: Skipbox (made by Fluxenic, the company that publishes this site) | Downloading and rebuilding at all: Docker layers, package caches, and the tool cache are already on the VM | A flat $39, $79, or $149 a month for 4, 8, or 16 vCPU, with unlimited minutes |
Fixes 1 to 4 work on the runner you have. Do them first.
1. Read the timing
Open the run, click the job, and read the time beside each step. GitHub's docs: "You can see the time it took for each step to run."
Sort one typical run by time and name each step's kind:
| The slow step | What it is waiting on | The fix |
|---|---|---|
| Set up Node, Python, or Java | Downloading the toolchain | Fix 6: a runner that keeps its tool cache |
| Install dependencies | Registry downloads | Fix 2 |
docker build | Rebuilding layers | Fix 3, or fix 6 |
| Compile, bundle | CPU | Fix 5, or fix 6 |
| Tests | CPU, or one long queue of test files | Fix 4, then fix 5 |
| Post-job cache save | Uploading a cache archive | Fix 6 |
A step that runs five commands shows one number. Split it into five steps, or put time in front of each command.
Then read the run as a whole. Ten 3-minute jobs that wait on one 14-minute job are a 14-minute pipeline.
2. Cache dependencies
Every job on a GitHub-hosted runner downloads its packages again. GitHub's setup actions cache them with one input:
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: "npm"
- run: npm ci
GitHub's caching reference lists the setup action for each package manager:
| Package manager | Action |
|---|---|
| npm, Yarn, pnpm | actions/setup-node |
| pip, pipenv, Poetry | actions/setup-python |
| Gradle, Maven | actions/setup-java |
| RubyGems | ruby/setup-ruby |
| Go | actions/setup-go |
| .NET NuGet | actions/setup-dotnet |
The cache has rules. GitHub's words:
- Seven days. "GitHub will remove any cache entries that have not been accessed in over 7 days."
- 10 GB a repository. "By default, the limit is 10 GB per repository." Past it: "the cache eviction policy will create space by deleting the caches in order of last access date, from oldest to most recent."
- Thrashing. "The cache eviction process may cause cache thrashing, where caches are created and deleted at a high frequency."
- Branches. "Workflow runs cannot restore caches created for child branches or sibling branches." A new feature branch gets only what the default branch saved.
- Pull requests. A cache saved by a pull request's run "can only be restored by re-runs of the pull request."
A cache hit isn't free either. The runner is a new VM each time, so every job downloads the archive and unpacks it, and saves a new one when the key changes.
3. Docker layer cache
A docker build on a new VM has no layers, so every instruction runs again. Docker's build action can keep the layers in GitHub's cache:
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: "<registry>/<image>:latest"
cache-from: type=gha
cache-to: type=gha,mode=max
Docker's docs call this backend "the recommended cache to use inside your GitHub Actions workflows, as long as your use case falls within the size and usage limits set by GitHub." Those are the 10 GB and the 7 days of fix 2, shared with your dependency caches.
Two settings decide whether it hits:
mode=max. Docker's docs: "Inmincache mode (the default), only layers that are exported into the resulting image are cached, while inmaxcache mode, all layers are cached, even those of intermediate steps." A multi-stage build needsmax.scope. "By default, it is set tobuildkit. If you build multiple images, each build will overwrite the cache of the previous, leaving only the final cache." Give each image its ownscope.
In the Dockerfile, order beats any flag. Copy the lockfile and install dependencies before you copy the source, so a source change reuses the dependency layer.
4. Parallelism and sharding
GitHub's workflow syntax: a run "is made up of one or more jobs, which run in parallel by default." Lint, typecheck, unit tests, and the build can be four jobs that finish in the time of the longest.
To split one long test suite, shard it across a matrix. Playwright's docs: "To shard the test suite, pass --shard=x/y to the command line."
jobs:
e2e:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: "npm"
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
And stop the runs a newer push replaced:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
What splitting costs:
- Setup repeats. Each shard checks out, restores caches, and installs browsers again. Four shards of a 12-minute suite take 3 minutes each plus that setup.
- Billed minutes go up. GitHub's pricing reference: "GitHub rounds the minutes and partial minutes each job uses up to the nearest whole minute." Each shard rounds up on its own.
- Your plan caps jobs at once. Standard GitHub-hosted runners run 20 jobs at a time on GitHub Free, 40 on Pro, 60 on Team, and 500 on Enterprise.
Sharding trades billed minutes for wall-clock time. On a per-minute runner you pay for the trade.
5. Larger runners
After the cuts, what remains is work, and most of it is CPU: compiling, bundling, and test workers. What a Linux x64 job gets on GitHub, read November 11, 2026:
| Runner | CPU | Memory | Disk | Per minute |
|---|---|---|---|---|
ubuntu-latest, private repository | 2 | 8 GB | 14 GB | $0.006 |
ubuntu-latest, public repository | 4 | 16 GB | 14 GB | Free |
| Larger runner, 4-core | 4 | 16 GB | 150 GB | $0.012 |
| Larger runner, 8-core | 8 | 32 GB | 300 GB | $0.022 |
| Larger runner, 16-core | 16 | 64 GB | 600 GB | $0.042 |
GitHub's pricing reference adds two conditions: "Larger runners are only available for organizations and enterprises using the GitHub Team or GitHub Enterprise Cloud plans," and "Larger runners are not free for public repositories."
Make the job use the cores it gets:
- run: make -j"$(nproc)"
More cores do nothing for a step that waits on the network. A larger GitHub runner is still a new VM for every job, so npm ci and docker pull download as before.
6. A runner that keeps its disk between jobs
Fixes 2 and 3 work around one fact: the runner's disk is empty at the start of every job. A runner that keeps its disk doesn't need the workaround. Nothing is restored, because nothing left.
GitHub's docs say self-hosted runners "don't need to have a clean instance for every job execution." You can run one yourself, and then the machine, its updates, and its cleanup are yours: see what a self-hosted GitHub Actions runner really costs.
Skipbox is that runner, operated for you. Each account gets its own Linux x64 VM, kept between jobs:
- What stays on the disk. Docker images, layers, and build cache; package caches for npm, pip, Go, Gradle, and Cargo; the tool cache that
setup-*actions fill. - What is wiped after every job. The runner's credentials, the work directory with your checkout, every container, and credential files such as
~/.npmrc. - No 7-day rule and no branch rule. The caches are files on your VM's disk, 80 GB on the smallest plan, not entries in GitHub's 10 GB cache.
- More cores.
runs-on: skipboxgives a job 4 vCPU and 8 GB.skipbox-8x16andskipbox-16x32give 8 and 16 vCPU on the larger plans. - A flat price. Skipbox is $39, $79, or $149 a month with unlimited minutes, so shards and re-runs don't raise the bill.
One caveat for Docker: docker build and docker compose build reuse the layers on the VM. The default builder of docker/setup-buildx-action starts empty in every job, so set driver: docker on it to use the machine's own cache.
Measure it on your own job
No benchmark of Skipbox has been published, so this post claims no speed for it. Measure your own job instead: the same commit, a warm run, and the step timings side by side on both runners.
What it costs
Skipbox M is $79 a month however many minutes you run. On GitHub Team, which includes $18 a month, Skipbox M is cheaper than GitHub's 8-core runner at $0.022 a minute above 4,410 job-minutes a month: ($79 + $18) ÷ $0.022. Skipbox S is cheaper than the 4-core runner above 4,750, and Skipbox L cheaper than the 16-core runner above 3,977.
Skipbox doesn't run macOS, Windows, or Arm jobs. A step that waits on a slow external service is the same speed on any runner.
To try it, change one job's runs-on: line to skipbox and push. Skipbox's trial runs one 4 vCPU job at a time for 7 days or 3,000 job-minutes, whichever comes first, with no card.
Start a free Skipbox trial on skipbox.dev
For every runner sold at a fixed monthly price, see flat-rate GitHub Actions runners. For per-minute prices across GitHub and 7 runner vendors, see how much GitHub Actions runners cost. And for what GitHub bills on a runner of your own, see are GitHub self-hosted runners free?
Related reading
- GitHub Actions pricing in November 2026: per-minute rates, what each plan includes, and a month's math
- Blacksmith alternatives by billing model, with prices
- AI coding agents and your CI bill: the GitHub Actions math
- Fix: an Actions budget is preventing further use
Frequently asked questions
How do I speed up GitHub Actions?
In this order: read the step timings to find the slow step, cache dependencies with the setup action's cache input, cache Docker layers with cache-to: type=gha,mode=max, split tests across parallel jobs with --shard, then give CPU-bound steps more cores. A runner that keeps its disk between jobs removes the downloads altogether: Skipbox is one, at a flat monthly price. Fluxenic, the company behind Convika, makes Skipbox.
Why is GitHub Actions so slow?
Two reasons. GitHub says each GitHub-hosted runner "is a new virtual machine," so every job downloads its tools and dependencies again and builds Docker images without the last build's layers. And on a private repository, ubuntu-latest has 2 CPUs and 8 GB (GitHub Docs, read November 11, 2026), half the 4 CPUs and 16 GB a public repository's job gets.
Why is my GitHub Actions cache not speeding things up?
Usually the cache misses or costs as much as it saves. GitHub removes entries that "have not been accessed in over 7 days," keeps 10 GB a repository, and doesn't share caches between sibling branches, so a new branch often starts cold. On a hit, the job still downloads and unpacks the archive, because the runner is a new VM every time.
Is there a GitHub Actions runner that keeps Docker layers between jobs?
Yes. Skipbox gives each account its own VM that is kept between jobs, so Docker images, layers, and build cache, package caches, and the tool cache are already on the disk when the next job starts. Skipbox is $39, $79, or $149 a month for 4, 8, or 16 vCPU with unlimited minutes, for Linux x64 jobs on public and private repositories. A job moves with one line: runs-on: skipbox. A self-hosted runner you operate yourself keeps its disk too.
Do larger runners make GitHub Actions faster?
For CPU-bound steps, yes: compilers, bundlers, and test workers use the cores they get. GitHub's larger Linux runners are $0.012 a minute for 4-core, $0.022 for 8-core, and $0.042 for 16-core (read November 11, 2026). Steps that wait on the network don't speed up, and a larger GitHub runner is still a new VM for every job.
Does sharding tests cost more on GitHub Actions?
Yes, in billed minutes. Each shard is its own job that repeats checkout and install, and "GitHub rounds the minutes and partial minutes each job uses up to the nearest whole minute." You trade minutes for wall-clock time. On a flat-price runner the trade costs nothing extra.
Sources
Read 2026-11-11 (Japan time).
- GitHub Docs: Using workflow run logs: the time each step took (checked 2026-11-11)
- GitHub Docs: Dependency caching reference: the setup actions, the 7 days, the 10 GB limit and eviction, branch and pull request scope (checked 2026-11-11)
- actions/setup-node: the
cacheinput (checked 2026-11-11) - Docker Docs: GitHub Actions cache backend and Cache storage backends:
type=gha,scope, theminandmaxmodes, the workflow example (checked 2026-11-11) - GitHub Docs: Workflow syntax: jobs in parallel by default (checked 2026-11-11)
- GitHub Docs: Control the concurrency of workflows and jobs:
cancel-in-progress(checked 2026-11-11) - Playwright Docs: Sharding:
--shardand the GitHub Actions matrix (checked 2026-11-11) - GitHub Docs: Actions limits: jobs at once by plan (checked 2026-11-11)
- GitHub Docs: GitHub-hosted runners reference:
ubuntu-lateston private and public repositories, a new VM per runner (checked 2026-11-11) - GitHub Docs: Larger runners reference and Actions runner pricing: larger runner sizes, per-minute rates, rounding up per job (checked 2026-11-11)
- GitHub Docs: Self-hosted runners: no clean instance needed for every job (checked 2026-11-11)
- GitHub Docs: GitHub Actions billing: GitHub Team's included compute (checked 2026-11-11)
- Skipbox: what stays on the VM and what is wiped, the 80 GB disk, sizes and labels, the buildx note, plans, the trial (checked 2026-11-11)
Related articles
- Are GitHub self-hosted runners free? Yes, in 2026Yes. GitHub bills nothing for self-hosted runner minutes (billing docs read November 11, 2026). Why AI answers say otherwise, and what you pay instead.
- Flat-rate GitHub Actions runners: unlimited minutes (2026)Every GitHub Actions runner with a fixed monthly price and unlimited minutes, in each vendor's own words, read November 11, 2026, with the break-even.
- What a self-hosted GitHub Actions runner really costsGitHub bills nothing for a self-hosted runner. You pay for the server, updates, a clean machine per job, security, and queues. Prices read November 11, 2026.