How to Cache Dependencies in GitHub Actions (and Speed Up CI)
CodexFuse Team · 2026-11-04T04:32:25.950Z · 13 min read min read
Learning how to cache dependencies in GitHub Actions is the fastest way to cut CI build times. Every workflow run starts on a fresh runner, which means every run re-downloads your full dependency tree — often hundreds of megabytes of npm packages, pip wheels, or Maven jars. Caching stores those files between runs so the runner restores them in seconds instead of downloading them again. If you are new to workflows themselves, start with our GitHub Actions beginner guide — the same pushes that trigger workflows are what fire your cached runs, and this guide assumes a working workflow file you want to speed up.
What is the fastest way to cache dependencies in GitHub Actions?
The fastest way to cache dependencies in GitHub Actions is the built-in caching of the official language setup actions: one line like cache: npm inside actions/setup-node@v7 skips the network download on repeat runs. When your ecosystem has no built-in option, use actions/cache@v6 explicitly with a key built from your lockfile hash. The payoff is large — RunsOn's test report (updated January 2026, cited by eastondev.com) measured npm installs dropping from 3 minutes to 40 seconds and pip installs from 45 seconds to 8 seconds, with cache hit rates of 70–90% depending on how well you design the key.
- Use the built-in cache first. The official setup actions —
actions/setup-node,setup-python,setup-go,setup-java, andsetup-dotnet— all ship acacheinput that handles paths and key design for you. One input beats a hand-rolled key for standard dependency caching. - Hash your lockfile, not your source tree. The classic key is
${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}. When dependencies do not change, the key does not change, and the cache restores instantly. - Add restore-keys as a safety net. A prefix fallback such as
${{ runner.os }}-npm-restores the newest partial cache after a lockfile change, so the install downloads only what is new instead of starting from nothing. - Cache the package manager's store, not your working tree. Caching
~/.npmand runningnpm ciis faster and more reliable than archivingnode_modulesdirectly. - Skip the install step on a cache hit.
actions/cachesets acache-hitoutput you can use in anif:condition to bypass the install entirely.
How does GitHub Actions caching actually work?
GitHub Actions caching works on a simple find–restore–save cycle: you declare a key, GitHub searches its cache storage for a match, restores the files if it finds one, and saves a new cache after the job completes if nothing matched. You can think of it as a snapshot of files keyed by a string you design. GitHub's own docs draw one important line: caching is for files that do not change often between runs, like package-manager dependencies — it is not a replacement for artifacts, which are for files produced by a job that you want to inspect or pass between jobs after the run ends.
Matching works in a fixed order. GitHub first searches for an exact match on your key. If nothing matches, it tries each of your restore-keys in order as prefix fallbacks, restoring the most recent cache whose key starts with that prefix. Keys have a maximum length of 512 characters, and anything longer fails the action. And one hard rule from the docs: you cannot change the contents of an existing cache — a new key means a new cache entry, so versioning your keys is how you version your caches.
Which setup actions already cache for you?
Six official setup actions ship built-in dependency caching, and the built-ins are usually better than a hand-rolled key because the maintainers keep the cache paths current as runner images change. All versions below were verified on the official release pages in November 2026. Use the built-in input whenever it covers your needs; drop down to explicit actions/cache only when you need custom paths, fallback keys, or caching something the setup action does not support.
| Action | Cache input | What it caches | On by default? |
|---|---|---|---|
actions/setup-node@v7 | cache: npm / yarn / pnpm | The package manager's global store — never node_modules | npm auto-caches when package.json declares a package manager; yarn and pnpm always need the explicit input |
actions/setup-python@v7 | cache: pip / pipenv / poetry | pip's global cache dir; pipenv's virtualenv; poetry's per-project virtualenvs | No — opt in per workflow |
actions/setup-go@v7 | cache: true | Go modules (~/go/pkg/mod) and build outputs (~/.cache/go-build), keyed partly off go.mod | Yes — the exception among the setup actions |
actions/setup-java@v6 | cache: maven / gradle / sbt | Three caches: dependency store (~/.m2/repository, ~/.gradle/caches), wrapper distributions, and the JDK itself | No — opt in per workflow |
actions/setup-dotnet@v6 | cache: nuget | Only the global-packages folder (~/.nuget/packages) — and the action throws if no packages.lock.json exists | No — opt in per workflow |
ruby/setup-ruby@v1 | bundler-cache: true | Installed gems, via bundle config --local path $PWD/vendor/bundle — it also runs bundle install for you | No — opt in per workflow |
Two notes that catch people out. First, actions/setup-ruby — the actions-org repository — is archived and deprecated; the canonical Ruby action is ruby/setup-ruby. Second, setup-node's npm auto-caching is newer than many older guides assume: if your package.json has a top-level or devEngines.packageManager field set to npm, caching is already on, and you pass package-manager-cache: false to turn it off.
How do you cache dependencies with actions/cache directly?
GitHub Actions cache dependencies through two mechanisms: the one-line built-ins covered above, and the explicit actions/cache action. You reach for the explicit action when the built-ins do not cover your case — custom paths, explicit fallback keys, or an ecosystem with no setup-action cache. As of November 2026 the current major is actions/cache@v6 (v6.1.0), and the v5 line moved to the Node 24 runtime, which needs an Actions Runner of version 2.327.1 or newer — worth knowing if you run self-hosted runners. The standard npm pattern looks like this:
- name: Restore npm cache
id: cache
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies
run: npm ci
For Node.js projects, the setup-node cache npm one-liner — cache: npm — is the canonical starting point, and the same shape works for pip:
- uses: actions/cache@v6
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-
The correct path matters per ecosystem: when you cache pip dependencies in GitHub Actions, ~/.cache/pip is the right Linux path (pip also respects XDG_CACHE_HOME); Go uses ~/go/pkg/mod plus ~/.cache/go-build, Maven uses ~/.m2/repository, and Gradle uses ~/.gradle/caches. When in doubt, prefer the setup action's built-in cache — it already knows these paths.
How do you design a cache key that actually hits?
A cache hits when the key is stable exactly as long as the dependencies are. Getting the cache key and restore-keys right is the core skill, and the three-part recipe is: the runner OS, the ecosystem name, and a hash of the lockfile — ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}. Including the OS matters because compiled artifacts differ across Linux, macOS, and Windows. Including the lockfile hash means a dependency change automatically creates a fresh cache instead of poisoning the old one.
The most common key mistakes are all avoidable. Hashing your source tree instead of the lockfile makes nearly every run miss, because sources change on almost every commit. Caching node_modules directly makes huge, slow archives — cache the package manager's store and let the install step assemble it. And keep keys under GitHub's 512-character limit; long keys from multi-glob hashFiles patterns fail the action outright. When a dependency change does miss the exact key, restore-keys is your second chance: the newest cache starting with the prefix restores, and the package manager downloads only the delta.
What are the GitHub Actions cache limits?
Four numbers govern every cache: 10 GB of storage per repository by default, automatic deletion of any entry untouched for 7 days, least-recently-used eviction when the budget fills up, and per-minute rate limits of 200 cache uploads and 1,500 downloads per repository. Those figures come straight from GitHub's official caching documentation, fetched this week. Practical caches use a fraction of the budget — most workflows cache one dependency directory and nothing else.
The 10 GB cap changed in November 2025: per the GitHub changelog, repositories can now exceed it on a pay-as-you-go model, with 10 GB still free for everyone. You can also raise the cap deliberately — enterprise owners, organization owners, or repository administrators can increase it, and newer cache policies let you tune the eviction size limit and the retention window. The takeaway is to keep your caches lean: cache dependency stores, not build outputs, and never archive directories that regenerate cheaply.
Is caching safe on pull requests?
Caching is safe on pull requests as long as you understand the scoping rules, which exist to prevent cache poisoning. A workflow run can restore caches created on its own branch or the repository's default branch (usually main) — it can never restore caches created for child or sibling branches. That means a PR mostly reads the default-branch cache, then saves its own caches against the PR's merge ref, which only that PR can see.
The security boundary is which triggers are allowed to create caches in the default branch's scope. Since June 2026, untrusted triggers — pull_request_target, issue_comment, and workflow_run from fork PRs — get a read-only cache token by default: they can restore but never overwrite the shared cache. Only trusted triggers like push, workflow_dispatch, repository_dispatch, delete, registry_package, page_build, and schedule can create or overwrite caches in that scope. If you need finer control, the newer cache-mode option offers four modes — read, write, write-only, and none — and the runner exposes the effective mode in the ACTIONS_CACHE_MODE environment variable.
Should you also cache Docker layers?
If your workflow builds container images, dependency caching alone will not speed the build — you need Docker layer caching alongside it. BuildKit can export a build's layer cache and import it on later builds through the GitHub Actions cache backend, with cache-from: type=gha pulling cached layers and cache-to: type=gha pushing the new ones. The two caches serve different jobs: the GHA cache holds dependency stores between runs, while the layer cache lets the builder reuse individual image layers and rebuild only what changed. If Docker builds are new to you, our Docker beginner guide covers the build side; this article covers the CI side.
How do you skip the install step on a cache hit?
You skip the install with the cache-hit output. actions/cache sets cache-hit to true when — and only when — the key matched exactly, so a conditional install looks like this: add id: cache to the cache step, then gate the install with if: steps.cache.outputs.cache-hit != 'true'. A partial hit from restore-keys sets cache-hit to false, which is correct: the install still runs, it just downloads only the delta.
This pattern matters most in repositories with high commit velocity — long-lived branches, monorepo lanes, and agent-driven pull requests, where an install runs on every single push. If you run coding agents in CI, for example through the Claude Code beginner guide's GitHub workflows, gating the install on a cache hit keeps agent-iterated PRs cheap. One caveat: the post-job save step only writes a new cache when nothing matched, so a workflow that always hits never rewrites its cache — the 7-day access rule keeps it alive as long as it keeps being used.
Why does your cache never hit?
A cache that never hits almost always fails for one of five reasons, so debug in this order. First, check the key: if it embeds a hash of a file that changes every commit — your source tree, a generated manifest, a timestamp file — it will miss every run. Second, check the branch: PR runs can only read the PR's branch and the default branch, so caches written on sibling feature branches are invisible. Third, check the budget: a repository over its 10 GB cap evicts least-recently-used entries first, which can silently delete the cache you need. Fourth, check cross-OS pollution: a cache written on windows-latest will not restore on ubuntu-latest if the OS is part of the key — by design. Fifth, check the action version and runner: actions/cache@v5 and newer need a modern runner (2.327.1+) because of the Node 24 runtime, and pinning to a stale major can fail silently on old self-hosted runners.
Sources
All facts, versions, and numbers in this guide were fetched this run (November 2026) via delegated live research — no statistics or versions were invented.
- GitHub Docs — "Caching dependencies to speed up workflows" (docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows): the 10 GB default limit, 7-day eviction, LRU policy, key/restore-keys matching order, 512-character key limit,
cache-hitoutput, cache immutability, branch scoping, read-only cache for untrusted triggers,cache-mode, and the 200/1,500 per-minute rate limits - GitHub Docs — "Dependency caching" concepts (docs.github.com/en/actions/concepts/workflows-and-actions/dependency-caching): cache vs artifacts use cases
- github.com/actions/cache releases — latest major v6 (v6.1.0); v5 release notes: Node 24 runtime, Actions Runner ≥ 2.327.1 requirement
- github.com/actions/setup-node README — latest v7 (v7.1.0);
cache: npm | yarn | pnpm; npm auto-caching via package.json; does not cachenode_modules - github.com/actions/setup-python README — latest v7 (v7.1.0);
cache: pip | pipenv | poetry, off by default - github.com/actions/setup-go README — latest v7 (v7.0.0);
cache: truedefault, caches modules and build outputs - github.com/actions/setup-java README — latest v6 (v6.1.0);
cache: maven | gradle | sbt; dependency, wrapper, and JDK caches - github.com/actions/setup-dotnet README — latest v6 (v6.1.0);
cache: nugetrestores onlyglobal-packages, requirespackages.lock.json - github.com/ruby/setup-ruby README — v1 (v1.336.0, Oct 30 2026);
bundler-cache: true, caches viavendor/bundle; actions/setup-ruby is archived - GitHub Changelog, 2025-11-20 — "GitHub Actions cache size can now exceed 10 GB per repository" (pay-as-you-go tiers)
- GitHub Changelog, 2026-06-26 — "Read-only Actions cache for untrusted triggers" (pull_request_target, issue_comment, fork-PR workflow_run)
- eastondev.com — "GitHub Actions Cache Strategy: Speed Up CI/CD Pipeline 5x" (benchmark figures attributed to RunsOn's January 2026 test report: npm 3 min → 40 s, yarn 2 min 30 s → 35 s, Docker 5 min → 1 min, pip 45 s → 8 s; 70–90% hit rates)
- runs-on.com — "Using caching to speed up GitHub Actions workflows" (page-1 intent reference)
- graphite.com — "How to use caching in CI/CD with GitHub Actions" (page-1 intent reference)
Frequently Asked Questions
Should I use the built-in cache in setup-node or actions/cache directly?
Use the built-in cache: npm input first. It handles the cache path and key design for you, and the maintainers keep those paths current as runner images change. Drop down to explicit actions/cache@v6 only when you need custom paths, custom fallback keys, or caching something the setup action does not support.
Does caching make CI builds insecure?
Not if you follow the scoping rules. PR runs can only read caches from their own branch or the default branch, and untrusted triggers like pull_request_target get a read-only cache token by default since June 2026, so a fork PR cannot overwrite your shared cache. The remaining risk is restoring a cache you no longer trust — bump the key prefix to start fresh when in doubt.
How much cache storage does GitHub give me?
Every repository gets 10 GB of cache storage at no cost, with least-recently-used eviction when the budget fills and automatic deletion of entries untouched for 7 days. Since November 2025, repositories can exceed 10 GB on a pay-as-you-go model, and enterprise, organization, or repository administrators can raise the cap deliberately.
Why is my cache-hit rate low?
The usual culprits are a key that changes every run (hashing the source tree instead of the lockfile), branch scoping (PRs cannot read sibling-branch caches), eviction from an over-full 10 GB budget, or cross-OS key pollution. Fix the key design first — stable key for stable dependencies, lockfile-hash versioning, and restore-keys as a fallback.
Can I cache dependencies on self-hosted runners?
Yes, but the cache service routes through GitHub's cache API either way, so network locality still matters. Keep your runner version current: actions/cache@v5 and newer run on the Node 24 runtime and need Actions Runner 2.327.1 or newer. Third-party runner providers also offer co-located cache backends that make restores several times faster.

