Skip to main content
CI/CD·8 min read

GitHub Actions Cache — Speed Up CI/CD Pipelines by 60%

Optimize GitHub Actions workflows with caching for dependencies, build artifacts, and Docker layers. Covers cache keys, restore strategies, cache invalidation, and real-world examples for Node.js, Python, Go, and Docker.

DT

DevOps Engineer & Technical Writer

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%.

GitHub Actions Cache Flow Workflow Run Check Cache key: hash(lockfile) restore-keys: prefix HIT Restore Cache → path MISS Install Deps npm ci / pip install Run Steps build / test Save Cache Cache Key = os + hashFiles('package-lock.json') Fallback = restore-keys prefix match

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

  • Caching without lockfile hash — Key like npm-${{ runner.os }} restores stale packages even when dependencies changed.
  • Not using restore-keys — Without fallback keys, any lockfile change results in complete cache miss.
  • Caching too much — Large directories that change frequently waste space and slow restore.
  • Hitting the 10 GB limit — Large Docker caches or many branch caches evict useful entries.
  • Not skipping install on hit — Always use if: steps.cache.outputs.cache-hit != 'true' to skip install when cache is restored.
  • Branch isolation confusion — PRs can read default branch caches but not write to them. Feature branches are isolated from each other.
  • Quick Reference

    LanguageCache PathKey Pattern
    Node.js (npm)<code class="inline-code">node_modules</code><code class="inline-code">hashFiles(&#39;package-lock.json&#39;)</code>
    Node.js (pnpm)<code class="inline-code">~/.pnpm-store</code><code class="inline-code">hashFiles(&#39;pnpm-lock.yaml&#39;)</code>
    Python (pip)<code class="inline-code">~/.cache/pip</code><code class="inline-code">hashFiles(&#39;requirements*.txt&#39;)</code>
    Python (venv)<code class="inline-code">.venv</code><code class="inline-code">hashFiles(&#39;requirements.txt&#39;)</code>
    Go<code class="inline-code">~/go/pkg/mod</code>, <code class="inline-code">~/.cache/go-build</code><code class="inline-code">hashFiles(&#39;go.sum&#39;)</code>
    Rust<code class="inline-code">~/.cargo</code>, <code class="inline-code">target</code><code class="inline-code">hashFiles(&#39;Cargo.lock&#39;)</code>
    DockerGHA 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(&#39;<em>*/</em>.gradle*&#39;)</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.

    ---