Engineering guide

Remote build cache for GitHub Actions: how actions/cache and artifacts fit

"GitHub Actions cache" can mean three different things that solve different problems: directory restore cache (actions/cache), run artifacts (actions/upload-artifact), and task-level remote build cache (Nx, Lerna, Turborepo, Bazel, Gradle). This guide explains the differences, when to combine them, and how to avoid the mistakes that make CI slower or less trustworthy.

What people mean by "GitHub Actions cache"

In practice, teams use the phrase GitHub Actions cache for different mechanisms:

  • actions/cache restores directories such as ~/.npm, .pnpm-store, ~/.gradle, or language package caches.
  • actions/upload-artifact stores files from a specific workflow run for humans or downstream jobs.
  • Remote build cache stores per-task outputs keyed by content hashes and can be reused by CI and developer machines.

They are complementary. A lot of confusion comes from expecting one layer to replace the others. If your goal is faster monorepo builds, start with how remote build caching works, then decide where actions/cache and artifacts still add value.

The three cache layers in GitHub Actions CI

Comparison of actions/cache, upload-artifact, and remote build cache.
MechanismBest atKey modelTypical misuse
actions/cacheReducing dependency install time by restoring package-manager caches.Manual keys and restore-keys, often lockfile-based.Caching build output directories and expecting task-level correctness.
actions/upload-artifactKeeping run outputs for download, review, and cross-job handoff.Named by workflow run, job, and artifact name.Treating artifacts as a build cache and reusing stale binaries by name.
Remote build cache (Nx/Turbo/Bazel/Gradle)Skipping task execution when input hashes match exactly.Content hash of task inputs, tool version, and declared env inputs.Assuming dependency restore cache is unnecessary once remote cache is enabled.

When to use actions/cache, artifacts, and remote cache

Use actions/cache for dependency restore caches

Save package-manager and tool download caches that are expensive to fetch but safe to reuse broadly. Good examples: npm/pnpm/yarn package caches, pip wheel caches, Maven/Gradle dependency caches.

Use artifacts for run deliverables

Upload test reports, coverage output, release bundles, SBOMs, and debug logs. Artifacts are for human and deployment workflows, not for deciding whether a task should execute.

Use remote build cache for the build graph itself

This is the mechanism that skips expensive monorepo tasks safely. A proper task cache understands task inputs and outputs, so it can restore exactly what a task would have produced. If you run Nx or Turborepo in Actions, this is the layer that removes redundant compile/test/lint work.

The practical model for most teams is: dependency caches with actions/cache, task cache with remote caching, artifacts for reporting and release handoff.

How to combine them in one workflow

A healthy GitHub Actions setup for a monorepo usually follows this order:

  • Restore dependency directories with actions/cache.
  • Install dependencies.
  • Run your build tool with remote cache enabled (Nx or Turborepo).
  • Upload reports and deployables as artifacts.

The key point is layering: dependency cache makes install faster; remote cache makes tasks disappear when unchanged; artifacts keep the outputs you need outside the job. If you want a managed remote-cache option with this model, Cachely is one, and the exact setup steps are in the docs.

Practical workflow snippets

1) Dependency restore cache (actions/cache)

- name: Restore npm cache
  uses: actions/cache@v6
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-

This speeds up dependency download. It does not replace task-level build cache.

2) Artifact upload for reports or deployables

- name: Upload test report
  uses: actions/upload-artifact@v7
  with:
    name: test-report-${{ github.run_id }}
    path: coverage/
    if-no-files-found: error

3) Nx remote cache in GitHub Actions

- name: Run affected tasks with remote cache
  env:
    NX_SELF_HOSTED_REMOTE_CACHE_SERVER: https://remote.cachely.dev
    NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN: ${{ secrets.NX_CACHE_TOKEN }}
  run: npx nx affected -t lint,test,build

4) Turborepo remote cache in GitHub Actions

- name: Turbo build with remote cache
  env:
    TURBO_API: https://remote.cachely.dev
    TURBO_TOKEN: ${{ secrets.TURBO_CACHE_TOKEN }}
    TURBO_TEAM: your-team-slug
  run: npx turbo run build test lint

Which actions/cache version should you pin?

Most workflows in the wild still say uses: actions/cache@v4, and most tutorials still teach it. Two major versions have shipped since, and the reason to care is the runner requirement rather than the feature list - the cache inputs (path, key, restore-keys) have not changed:

  • v4 - runs on Node 20. Still functional and the safest pin if you operate older self-hosted runners.
  • v5 - moved to the Node 24 runtime and therefore requires runner version 2.327.1 or newer. GitHub-hosted runners satisfy this automatically; self-hosted fleets must be updated first, and this is the upgrade that breaks people.
  • v6 - an internal migration to ESM on top of the v5 runtime baseline. No workflow-visible input changes.

The practical rule: pin the major (@v6) so you get patches without surprises, and if you run self-hosted runners, confirm the runner version before moving off v4. Pinning a full commit SHA is stricter still and worth it for security-sensitive repositories, at the cost of doing your own updates. The same Node-runtime cadence applies to actions/checkout, actions/setup-node, and actions/upload-artifact, so upgrade them as a set rather than one at a time.

None of this changes the ceiling. A newer actions/cache restores your dependency directory marginally faster; it still cannot skip a compile, test, or lint task, because it has no idea what those tasks read. If your pipeline is slow because it rebuilds unchanged code, the version you pin is not the variable that matters - the cache layer is.

Common mistakes and why they hurt

  • Overly broad cache keys in actions/cache. Keys like node-modules-main restore old dependency state across commits and branches, causing hard-to-reproduce behavior.
  • Stale dependency caches that never rotate. Missing lockfile hash components means cache content drifts from declared dependencies.
  • Expecting actions/cache to replace remote task cache. Directory restoration cannot reason about the task graph, so builds still execute work that a task cache would skip.
  • Using artifacts as cache storage. Artifacts are run-scoped deliverables, not deterministic task-replay storage.
  • No read-only token split for untrusted CI. Pull-request and fork pipelines should not share write tokens with protected branches. Use read-only tokens for untrusted builds to reduce cache-poisoning risk.

For deeper context on safe remote-caching trust models, see security and compare.

Troubleshooting checklist

actions/cache miss rate is unexpectedly high

  • Check that your key includes lockfiles and OS/arch where needed.
  • Use restore-keys only for safe fallbacks, not broad cross-branch reuse of mutable directories.
  • Verify paths actually exist at restore and save steps.

Dependency cache restores but installs are still slow

  • Confirm you are caching package-manager cache directories, not just installed project directories.
  • Validate the package manager config is reading that cache path on Actions runners.

Remote cache hit rate is low in CI

  • Audit task inputs for unstable env vars, timestamps, or path differences.
  • Ensure CI and local runs use matching tool versions and lockfile state.
  • Measure misses by target to find the noisy input source before changing key scope.

Build looked "cached" but output is wrong

  • Check whether an artifact was reused by name instead of rebuilding with task cache semantics.
  • Recheck your declared task outputs and input hashing config in the build tool.

Remote cache vs GitHub cache: decision guide

If your pain is mostly dependency download time, actions/cache might be enough. If your pain is repeated compile/test/lint work in a monorepo, you need remote task caching. Most teams with non-trivial CI use both. Use artifacts to publish outputs and reports, not to drive cache-hit decisions.

For a broader remote-cache implementation guide, read /remote-cache. To diagnose disappointing remote hits, use the cache hit-rate guide. For implementation details and rollout steps, use /docs. For pricing and alternatives, see /pricing and /compare.

Related guides

Need faster GitHub Actions builds without brittle cache hacks?
Keep dependency caches for installs, add task-level remote cache for build/test/lint reuse.
Start freeSee pricing
FAQ

GitHub Actions cache: frequently asked questions

Is actions/cache the same as a remote build cache?
No. actions/cache restores directories by keys you define, usually for dependency caches. A remote build cache stores task outputs keyed by content hashes of the task inputs and can skip task execution entirely.
When should I use actions/upload-artifact?
Use artifacts for run outputs you want to keep or pass to later jobs: coverage reports, test logs, binaries, and release bundles. Artifacts are not a substitute for deterministic task cache hits.
Can I use actions/cache and a remote cache together?
Yes. Most teams should. Use actions/cache to speed dependency restore and use remote caching to skip compile/test/lint tasks when input hashes match. The two layers solve different bottlenecks.
Why are my actions/cache restores stale or flaky?
The usual causes are broad keys, missing lockfile hashes, or restore-keys that pull in old mutable directory states from unrelated branches. Include deterministic key inputs and keep fallback scope narrow.
Why does CI still run full builds after restoring actions/cache?
Because restoring dependency directories does not mean build tasks are cached. actions/cache helps install time; task-level remote caching is what skips repeated build, test, and lint work.
Should I use artifacts as a CI cache?
Generally no. Artifacts are run-scoped deliverables for humans or downstream jobs, not content-addressed task replay storage. Using artifacts as a cache usually leads to stale output reuse and weaker correctness.
What is the biggest security mistake with CI cache setup?
Letting untrusted pull-request or fork builds write to caches trusted builds later read. Use read-only tokens for untrusted builds and separate write tokens for protected branches.
Do I need a managed service for remote caching?
Not always. You can self-host if you are willing to own uptime, token security, immutability controls, and operational maintenance. Managed options are for teams that want the cache benefits without running that infrastructure.
Which actions/cache version should I use?
Pin the latest major, currently actions/cache@v6. The cache inputs (path, key, restore-keys) are unchanged since v4, so the upgrade is about the runtime: v5 moved to Node 24 and therefore requires Actions Runner 2.327.1 or newer, and v6 migrated the action internals to ESM on that same baseline. GitHub-hosted runners meet the requirement automatically; if you operate self-hosted runners, update them before moving off v4.
How do I restore a cache without saving one in GitHub Actions?
Use actions/cache/restore, the restore-only variant, when a job should read an existing cache but never write a new entry - for example an untrusted pull-request job. Pair it with actions/cache/save in a trusted job that populates the entry. The same read-only versus read-write split is the core security pattern for remote build caches too.