Belum ada versi bahasa Indonesia, jadi tampil dalam bahasa Inggris.

How to speed up GitHub Actions: 6 fixes, in order

Convika Team12 min read

  • ci
  • github-actions
  • self-managed
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

OrderFixWhat it removesWhat it costs
1Read the step timingsGuessingNothing
2Cache dependenciesRegistry downloads on a cache hitA 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"
3Cache Docker layersRebuilding unchanged layersA cache export and import over the network, inside the same 10 GB
4Run in parallel and shardWaiting for one long jobMore billed minutes: every job repeats its setup and rounds up to a whole minute
5A larger GitHub runnerSlow CPU-bound stepsA higher rate: $0.012 a minute for 4-core, $0.022 for 8-core, $0.042 for 16-core
6A 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 VMA 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 stepWhat it is waiting onThe fix
Set up Node, Python, or JavaDownloading the toolchainFix 6: a runner that keeps its tool cache
Install dependenciesRegistry downloadsFix 2
docker buildRebuilding layersFix 3, or fix 6
Compile, bundleCPUFix 5, or fix 6
TestsCPU, or one long queue of test filesFix 4, then fix 5
Post-job cache saveUploading a cache archiveFix 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 managerAction
npm, Yarn, pnpmactions/setup-node
pip, pipenv, Poetryactions/setup-python
Gradle, Mavenactions/setup-java
RubyGemsruby/setup-ruby
Goactions/setup-go
.NET NuGetactions/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: "In min cache mode (the default), only layers that are exported into the resulting image are cached, while in max cache mode, all layers are cached, even those of intermediate steps." A multi-stage build needs max.
  • scope. "By default, it is set to buildkit. If you build multiple images, each build will overwrite the cache of the previous, leaving only the final cache." Give each image its own scope.

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:

RunnerCPUMemoryDiskPer minute
ubuntu-latest, private repository28 GB14 GB$0.006
ubuntu-latest, public repository416 GB14 GBFree
Larger runner, 4-core416 GB150 GB$0.012
Larger runner, 8-core832 GB300 GB$0.022
Larger runner, 16-core1664 GB600 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: skipbox gives a job 4 vCPU and 8 GB. skipbox-8x16 and skipbox-16x32 give 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?

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).