Skipbox

Speed up a pnpm and Turborepo monorepo in GitHub Actions (astro: 528 s to 30 s)

In a pnpm and Turborepo monorepo on GitHub Actions, most of a slow job is two steps: pnpm install on a fresh VM, and turbo run build test running tasks whose results the VM doesn't have. Fix them in this order. Run only the packages a change touches (turbo run build --affected, with a checkout that has history). Give turbo a cache that outlives the job, since its results live in .turbo/cache on a machine GitHub deletes after every job: Remote Caching, or .turbo restored with actions/cache. And keep the pnpm store. On GitHub's runners each of those caches is a download every job. On a machine that keeps its disk they are simply there: astro's CI job took 528 s on GitHub and 30 s on a Skipbox own machine, which replayed all 40 build tasks from turbo's kept cache.

Skipbox runs your jobs on your account's own machine, which keeps the pnpm store, the tools and the checkout's .turbo between jobs, with no remote cache and no cache keys. Change one line, runs-on: skipbox, for a flat $39 a month with unlimited minutes.

Where a monorepo job spends its time

1. Run only what changed

From Turborepo's CI guide: turbo run build --affected runs tasks only in packages changed against the base branch. In GitHub Actions it reads GITHUB_BASE_REF on pull requests and GITHUB_EVENT_PATH on pushes. With a shallow clone every package counts as changed, so check out the history:

- uses: actions/checkout@v4
  with:
    fetch-depth: 0
    filter: blob:none

turbo query affected --packages web tells a job whether to run at all, so a job for an untouched app can skip its install.

2. Give turbo a cache that outlives the job

Turborepo stores task results in .turbo/cache on the machine. A GitHub-hosted job's machine is deleted when it ends, so in CI that cache starts empty every time. Two ways around it, from Turborepo's GitHub Actions guide:

  • Remote Caching. Set TURBO_TOKEN and TURBO_TEAM (Vercel Remote Cache, or a self-hosted server). Every hit is a download from it.
  • actions/cache on .turbo. Key on ${{ runner.os }}-turbo-${{ github.sha }} with restore-keys: ${{ runner.os }}-turbo-. It saves a new entry on every run, inside GitHub's 10 GB per repository with everything else the repository caches.

3. Keep the pnpm store

actions/setup-node with cache: pnpm, or actions/cache on pnpm store path, restores the store keyed on pnpm-lock.yaml. It doesn't restore node_modules, so pnpm still links every package. pnpm's CI docs put it plainly: caching the store "is not guaranteed" to make installation faster. In CI pnpm switches to --frozen-lockfile by itself.

4. Use the cores

turbo runs independent tasks in parallel and each build or test tool starts its own workers, so a monorepo uses every core it gets. A private repository's ubuntu-latest has 2.

What each fix costs on GitHub's runners

  • Remote Caching is another service and another token, and every cache hit crosses the network.
  • actions/cache for .turbo writes an entry every run and competes for the same 10 GB as the pnpm store, Docker's layers and every branch's caches; entries unused for 7 days go.
  • A bigger runner is a larger runner on GitHub Team or Enterprise Cloud, billed per minute.

When the fix is a machine that keeps its disk

astro's own CI, its workflow unchanged, from our runs of real projects:

The own machine kept the checkout's .turbo and the pnpm store from the last job, so turbo found every task's result with nothing to download. turbo replays a task only when its inputs hash the same, so a real commit reruns the packages it touches and replays the rest.

Skipbox keeps the monorepo's state

Each plan comes with your account's own machine, kept between jobs:

  • The pnpm store stays in ~/.local/share/pnpm/store, with npm's cache and each tool's ~/.cache. pnpm install links from the disk instead of downloading.
  • turbo's cache stays. The checkout's top-level .turbo is kept between jobs when turbo.json is turbo 2's: a tasks key, no pipeline, and no "envMode": "loose". No TURBO_TOKEN, no cache step.
  • node_modules is deleted after each job, so a Node version matrix sharing one checkout never loads a native addon built for another Node.
  • The tools stay: the Node versions setup-node installed, pnpm itself, Docker's images and layers.

At least 7× GitHub's cache room on S. A job picks its size by label:

Minutes are unlimited. At 20,000 minutes a month on 4 vCPU, GitHub bills $222; Skipbox S is $39, $183 less.

Moving is one line

jobs:
  build:
    runs-on: skipbox # was ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          filter: blob:none
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install
      - run: pnpm turbo run build test --affected
  1. Sign in to Skipbox with GitHub.
  2. Install the Skipbox GitHub App and pick the repositories.
  3. Change runs-on and push.

Keep the cache steps as they are; what they save stays next to the machine.

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, and so are the git credentials, hooks and untracked files in the kept checkout. Clean every job gives each job a fresh machine instead.
  • Code from people without write access (such as pull requests from forks) runs on a fresh VM that reads your caches but can't change them.

The details are in how jobs are isolated.

Try it on the monorepo

The trial is free for 7 days or 3,000 job-minutes, whichever comes first, and takes no card. Move the build job to runs-on: skipbox, run it twice, and put the second run's install and turbo lines (cached against executing) next to GitHub's.

FAQ

How do I speed up a large pnpm/Turborepo monorepo in GitHub Actions?

Three things, in order. Run only what changed: turbo run build --affected, with a checkout that has history (fetch-depth: 0 and filter: blob:none). Give turbo a cache that survives the job: Turborepo's Remote Cache, or .turbo restored with actions/cache. And keep the pnpm store: setup-node's cache: pnpm or actions/cache on the store path. On GitHub's fresh VMs each of those is a download every job; on a machine that keeps its disk they are already there. astro's CI took 528 s on GitHub and 30 s on a Skipbox own machine that keeps turbo's cache and the pnpm store.

Why is pnpm install slow in GitHub Actions even with the store cached?

Because a restored store is still an archive downloaded and unpacked on a new VM, and pnpm then links node_modules for every package of the workspace. pnpm's own CI docs say caching the store is not guaranteed to make installation faster. A store that is already on the machine's disk skips the download; only the linking is left.

Does Turborepo cache work in GitHub Actions?

Only if the cache outlives the job. Turborepo keeps task results in .turbo/cache on the machine, and a GitHub-hosted job's machine is gone when it ends. So CI needs Remote Caching (TURBO_TOKEN and TURBO_TEAM, Vercel's or a self-hosted one) or an actions/cache step for .turbo. On a Skipbox own machine the checkout's .turbo stays between jobs for turbo 2 projects, so turbo replays unchanged tasks with no remote cache.

Does turbo --affected work in GitHub Actions?

Yes. Turborepo reads GITHUB_BASE_REF on pull requests, and GITHUB_EVENT_PATH on pushes, to find the base to compare against. It needs the history: with a shallow clone every package counts as changed. Set fetch-depth: 0 and filter: blob:none on actions/checkout.

What does Skipbox keep for a Turborepo monorepo?

The pnpm store, npm's and each tool's caches, the tools setup-node installs, Docker's layers, and the checkout with its top-level .turbo when turbo.json is turbo 2's (a tasks key, no pipeline, not envMode loose). node_modules is deleted after each job, so install runs, from the store on the disk. turbo replays a task only when its inputs hash the same, so a commit reruns what it touches.

How much does it cost to try?

Nothing. The trial is 7 days or 3,000 job-minutes, whichever comes first, with no card. Move the monorepo's build job to runs-on: skipbox, run it twice, and compare the second run's install and turbo steps with GitHub's.

Updated November 13, 2026

Faster CI/CD with an always-warm cache

Your warm runner at a flat price

1/12 the cost of GitHub at full use. Flat price, so run it all day. 7× GitHub's cache space.

  • No card
  • Sign in with GitHub
  • Cancel anytime