Why GitHub Actions is slow, and slower than your laptop
A test job that runs in 6 minutes on your laptop and 18 on GitHub Actions is paying for two differences. Your laptop keeps its packages, tools, build output and Docker layers on disk and has 8 or more cores. A GitHub-hosted job starts on a blank VM, downloads and installs all of it again, and on a private repository's ubuntu-latest runs on 2 CPUs and 8 GB. So the extra 12 minutes are setup plus tests on a quarter of the cores. Read the job's step times: when setup dominates, the fix is a machine that keeps its state; when the test step does, it is more cores and a test runner told to use them.
Skipbox is a GitHub Actions runner that works like your laptop: the machine stays between jobs, so what one job installed and built is still on the disk for the next, and a job gets 4 to 16 vCPU. Change one line, runs-on: skipbox, for a flat $39 a month with unlimited minutes.
Your laptop against the runner
| Your laptop | GitHub's ubuntu-latest, private repository | |
|---|---|---|
| CPU and memory | Usually 8 or more cores | 2 CPUs, 8 GB (GitHub-hosted runners) |
| Disk | Hundreds of GB, with yesterday's state on it | 14 GB of SSD, empty of your project |
| Packages | Installed | Downloaded and installed again, or restored from a cache archive |
| Build output | Incremental: only what changed compiles | Compiled whole |
| Docker | Layers from the last build | No layers |
A public repository's ubuntu-latest gets 4 CPUs and 16 GB. Check what your job sees with a first step:
- run: nproc && free -h && df -h
Every job starts from a blank VM
A GitHub-hosted runner is a new virtual machine for every job. Nothing the last job downloaded or built is on it, so each job rebuilds the same state:
| Step | GitHub-hosted runner | Skipbox |
|---|---|---|
| Set up Node, Python, Java | Downloads the toolchain | Finds it in the tool cache from the last job |
| Install dependencies | Downloads and unpacks the cache archive; on a miss, the whole registry download | Reads the package manager's cache from the disk |
| Build the Docker image | Starts with no layers and rebuilds every step | Reuses the last build's layers |
| Compile | Every crate or package from scratch | Cargo's target/ and turbo's cache from the last job |
| Run the tests | 2 CPUs on a private repository's ubuntu-latest | 4 vCPU and up |
The first four rows produce nothing new. They put the machine back where it was yesterday.
GitHub's cache softens this without changing it. It holds 10 GB per repository, deletes entries no job read for 7 days, and a new feature branch can't read another feature branch's cache. Even a hit downloads and unpacks an archive on every run (cache size limit).
The cores the tests get
Test runners size themselves to the machine. Jest starts the cores minus one, which is one worker on 2 CPUs. Playwright defaults to half the cores and recommends one worker in CI. pytest runs one process until -n auto. So a suite that spreads over 8 cores on your laptop runs one or two at a time on the runner, and no workflow setting changes the cores it has.
What the wait costs you
Say setup takes 3 minutes of each job and your team runs 100 jobs a day. That is 300 minutes a day of runner time spent putting the machine back, and every pull request waits on its share. Reviews start later, merges land later, and engineers switch to something else while they wait.
On per-minute billing those minutes are on the invoice. A 4 vCPU runner, by monthly minutes:
| Minutes a month (4 vCPU) | GitHub | Skipbox S | You save each month |
|---|---|---|---|
| 10,000 | $102 | $39 | $63 |
| 20,000 | $222 | $39 | $183 |
| 30,000 | $342 | $39 | $303 |
GitHub's figures are the bill after GitHub Team's included usage. Put in your own minutes on the calculator on the home page; no sign-in needed.
And the number of runs only goes up:
- AI coding agents. Every agent push starts CI. The waiting and the bill grow with pushes, not headcount.
- More tests. An E2E suite or a matrix repeats the setup in every job it adds.
- Budgets. Cap the bill with a GitHub budget, and jobs stop starting mid-month, which stops merges.
Speeding it up yourself
The workflow fixes come first, and they cost nothing: cancel superseded runs with a concurrency group, skip jobs whose files didn't change, and drop needs: that make independent jobs wait (the levers in order). Past those, keeping GitHub-hosted runners fast means work like this:
- Keying caches on lockfile hashes and tuning
restore-keysuntil they hit - Adding buildx and exporting Docker's cache with
cache-to, inside the same 10 GB - Sharding tests, where every shard repeats the setup and the billed minutes go up
- Or running your own self-hosted runner: provisioning, OS updates, cleaning up between jobs, and fixing it when it goes down
Each one works around a machine that forgets everything. Skipbox keeps the machine, so none of it is needed.
What changes on Skipbox
The setup stays on the disk. Your jobs run on your account's own machine, and it stays between jobs. Docker images, layers and build cache; package caches for npm, pnpm, pip, Gradle and more; the tools setup-node and friends install; your checkout with Cargo's target/ and turbo's cache. At least 7× GitHub's cache room, with no expiry after 7 days and no branch rule.
Bigger machines at a flat price. Compilers and test runners start one worker per core, so more cores shorten the CPU-bound steps.
| Label | Machine | Price |
|---|---|---|
skipbox | 4 vCPU · 16 GB | $39 a month (S) |
skipbox-8x16 | 8 vCPU · 16 GB | $79 a month (M) |
skipbox-16x32 | 16 vCPU · 32 GB | $149 a month (L) |
Minutes are unlimited on every plan. Skipbox jobs use no GitHub minutes, so a GitHub budget never stops them.
No machine to look after. Skipbox provisions the machine, updates the OS and tools, cleans up after every job, runs a health check and rebuilds a machine that fails it.
Moving is one line
jobs:
build:
runs-on: skipbox # was ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
- Sign in to Skipbox with GitHub.
- Install the Skipbox GitHub App and pick the repositories.
- Change
runs-onand push.
What the first job installs is on the disk for the second.
Security
- The machine is your account's alone. No other company's jobs run on it.
- After every job, the runner's credentials, processes and containers are wiped. From 13 November 2026, your checkout and its build output (Cargo's
target/) stay on your own machine for your next jobs; Clean every job gives each job a fresh machine. - Code from people without write access (such as pull requests from forks) runs on a fresh, throwaway machine.
The details are in how jobs are isolated.
Try it on your slowest job
The trial is free for 7 days or 3,000 job-minutes, whichever comes first, and takes no card. Move your slowest job to runs-on: skipbox, run it twice in a row, and put the step timings next to GitHub's.
FAQ
Why is GitHub Actions so slow?
Each job on a GitHub-hosted runner starts on a new VM with nothing from the last run, so it downloads its tools, reinstalls its dependencies, rebuilds its Docker images and compiles from scratch before your tests start. And a private repository's ubuntu-latest has 2 CPUs and 8 GB, so the CPU-bound steps run two workers at a time. Skipbox runs your jobs on your account's own machine, keeps that state on its disk, and gives the job 4 vCPU or more.
My tests take 6 minutes locally and 18 minutes on GitHub Actions. Why?
Compare the two machines. Your laptop has its packages installed, its build output and Docker layers on disk, and usually 8 or more cores. The CI job starts on an empty 2-CPU VM, so the extra minutes are setup (downloads, installs, image builds) plus tests running on a quarter of the cores. Read the job's step times to see which: if setup dominates, keep the state; if the test step does, give it cores and let the test runner use them.
I added actions/cache. Why is my workflow still slow?
A cache hit still downloads and unpacks an archive on every job, because the VM is new each time. And GitHub's cache holds 10 GB per repository, drops entries unused for 7 days, and can't be read from a sibling branch. On Skipbox the package caches and Docker layers are already on the machine's disk, with no keys to manage.
Should I switch runners or optimize the workflow first?
Do the free workflow fixes first: cancel superseded runs, skip jobs whose files didn't change, and remove needs: that make independent jobs wait. What is left after that, setup repeated on every job and steps that keep every core busy, is the machine's, and a runner that keeps its disk and has more cores removes it with one runs-on line.
Do I have to rewrite my workflows to use Skipbox?
No. For Linux jobs, change runs-on to skipbox. The machine runs Ubuntu 24.04 with Docker, Node.js and Python, and your steps and actions run as they are.
Can I try it on my own slow job for free?
Yes. The trial is 7 days or 3,000 job-minutes, whichever comes first, with no card. Move your slowest job to runs-on: skipbox, run it twice in a row, and compare the step timings with GitHub's.
Updated November 13, 2026