The Problem
Your CI pipeline takes 12 minutes. Eight of those are downloading the same packages that have not changed since the last run. Every push triggers a full dependency install, eating into developer productivity and Actions minutes.
Caching stores artifacts between runs. A well-configured cache cuts pipeline duration by 50-70%.
How GitHub Actions Cache Works
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
- Exact key match — Cache restored, hit
- Restore key match — Partial/stale cache restored
- No match — Cache miss, install from scratch
- After workflow — New cache saved on miss
Limits: 10 GB per repository, 7-day eviction for unused entries.
Node.js Caching
Built-in (setup-node)
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
Manual with skip on hit
- uses: actions/cache@v4
id: npm-cache
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- if: steps.npm-cache.outputs.cache-hit != 'true'
run: npm ci
With build cache
- uses: actions/cache@v4
with:
path: |
node_modules
.next/cache
key: deps-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**') }}
restore-keys: |
deps-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-
deps-${{ runner.os }}-
Python Caching
Built-in
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -r requirements.txt
Virtual environment cache
- uses: actions/cache@v4
id: venv-cache
with:
path: .venv
key: venv-${{ runner.os }}-py3.12-${{ hashFiles('requirements.txt') }}
- if: steps.venv-cache.outputs.cache-hit != 'true'
run: |
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
- run: |
source .venv/bin/activate
pytest
Go Caching
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- run: go build ./...
- run: go test ./...
Manual
- uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: go-${{ runner.os }}-${{ hashFiles('go.sum') }}
restore-keys: |
go-${{ runner.os }}-
Docker Layer Caching
GHA cache backend
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Registry cache
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/myapp:latest
cache-from: type=registry,ref=${{ env.REGISTRY }}/myapp:cache
cache-to: type=registry,ref=${{ env.REGISTRY }}/myapp:cache,mode=max
Cache Key Strategies
Hash-based
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
key: pip-${{ runner.os }}-${{ hashFiles('requirements*.txt') }}
key: go-${{ runner.os }}-${{ hashFiles('go.sum') }}
Multi-level restore
key: build-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
restore-keys: |
build-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-
build-${{ runner.os }}-
Weekly rotation
- id: date
run: echo "week=$(date +%V)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
key: npm-${{ runner.os }}-week-${{ steps.date.outputs.week }}-${{ hashFiles('package-lock.json') }}
Caching Across Jobs
jobs:
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: nm-${{ hashFiles('package-lock.json') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
test:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: node_modules
key: nm-${{ hashFiles('package-lock.json') }}
- run: npm test
build:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: node_modules
key: nm-${{ hashFiles('package-lock.json') }}
- run: npm run build
Managing Caches
# List caches
gh cache list
# Delete specific cache
gh cache delete <cache-id>
# Delete all caches for a branch
gh cache list --ref refs/heads/feature-branch | \
awk '{print $1}' | xargs -I {} gh cache delete {}
Complete Optimized Example
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: actions/cache@v4
id: deps
with:
path: |
node_modules
~/.cache/Cypress
key: deps-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- if: steps.deps.outputs.cache-hit != 'true'
run: npm ci
- run: npm run lint
- run: npm run build
- run: npm test
Common Mistakes
npm-${{ runner.os }} restores stale packages even when dependencies changed.if: steps.cache.outputs.cache-hit != 'true' to skip install when cache is restored.Quick Reference
| Language | Cache Path | Key Pattern |
|---|---|---|
| Node.js (npm) | <code class="inline-code">node_modules</code> | <code class="inline-code">hashFiles('package-lock.json')</code> |
| Node.js (pnpm) | <code class="inline-code">~/.pnpm-store</code> | <code class="inline-code">hashFiles('pnpm-lock.yaml')</code> |
| Python (pip) | <code class="inline-code">~/.cache/pip</code> | <code class="inline-code">hashFiles('requirements*.txt')</code> |
| Python (venv) | <code class="inline-code">.venv</code> | <code class="inline-code">hashFiles('requirements.txt')</code> |
| Go | <code class="inline-code">~/go/pkg/mod</code>, <code class="inline-code">~/.cache/go-build</code> | <code class="inline-code">hashFiles('go.sum')</code> |
| Rust | <code class="inline-code">~/.cargo</code>, <code class="inline-code">target</code> | <code class="inline-code">hashFiles('Cargo.lock')</code> |
| Docker | GHA backend | <code class="inline-code">type=gha</code> in build-push-action |
| Gradle | <code class="inline-code">~/.gradle/caches</code> | <code class="inline-code">hashFiles('<em>*/</em>.gradle*')</code> |
Summary
Caching transforms slow pipelines into fast feedback loops. Use lockfile hashes for keys, add restore-keys for fallback, and skip installs on hits. Monitor hit rates in the GitHub Actions UI and clean stale caches regularly. For Docker builds, use the GHA cache backend with BuildKit.
---
Frequently Asked Questions
How does caching work in GitHub Actions?
GitHub Actions caching stores files between workflow runs using a key-based system. When a cache key matches, files are restored to the specified path, skipping expensive operations like dependency installation. Caches are scoped to a branch and can fall back to the default branch. The actions/cache action handles save/restore automatically.
What should I cache in GitHub Actions?
Cache dependency directories: node_modules or ~/.npm for Node.js, ~/.m2/repository for Maven, ~/.cache/pip for Python, and build output directories. Also cache Docker layers using docker/build-push-action with GitHub Actions cache backend. Don't cache files that change every run — the restore overhead exceeds the time saved.
How do I fix "cache miss" in GitHub Actions?
Cache misses happen when the key doesn't match any existing cache. Usually the hash input has changed — verify your key includes the right lock file (hashFiles('**/package-lock.json')). Use restore-keys as a fallback prefix to match partial keys. Check that caches haven't expired (GitHub deletes caches unused for 7 days or exceeding 10GB total).
How much time can caching save in CI/CD pipelines?
Caching typically reduces pipeline duration by 30-70% depending on the workload. A Node.js project installing 500MB of node_modules goes from 45 seconds to 5 seconds with cache. Docker layer caching can reduce build times from 5-10 minutes to under 1 minute for unchanged layers. The impact is largest for projects with heavy dependencies.
What is the GitHub Actions cache size limit?
Each repository can store up to 10GB of caches total. Individual cache entries have no explicit size limit but are subject to the repository total. GitHub automatically evicts the least recently used caches when the limit is reached. Monitor cache usage via the Actions tab and use specific keys to avoid storing redundant caches.
---
Related Resources
- Git Cheatsheet — 106 git commands by workflow
- DORA Metrics Calculator — DORA metrics calculator
- Production Reference Architectures — GitOps CI/CD reference architecture
- GitHub Actions CI/CD Complete Guide — Full CI/CD pipeline setup guide
- Zero Downtime Deployment Strategies — Deployment patterns for faster releases