10.2 Caching Dependencies with `actions/cache`

Key Takeaways

  • Dependency caching via `actions/cache@v4` persists package dependencies, build trees, and compilers across workflow runs to drastically shorten execution duration and compute minute consumption.
  • Cache keys must be unique and deterministic, dynamically constructed using operating system and package lockfile hashes (e.g., `${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}`).
  • Sequential `restore-keys` fallback matching allows partial prefix hits when an exact key match fails, downloading the most recently saved related cache and reducing fresh download overhead.
  • Built-in caching in official setup actions (`actions/setup-node`, `actions/setup-python`, `actions/setup-go`, `actions/setup-java`) automatically resolves paths and lockfiles with a single `cache: <manager>` parameter, eliminating verbose boilerplate.
  • GitHub enforces a 10 GB cache storage limit per repository and an automated LRU (Least Recently Used) eviction policy, purging unaccessed caches after 7 days or when total repository usage exceeds 10 GB. Branch cache isolation ensures feature branches can read the default branch's cache but cannot overwrite or access peer branch caches.
Last updated: August 2026

Caching Dependencies with actions/cache

Continuous Integration workflows frequently perform redundant, time-consuming tasks across consecutive runs: downloading hundreds of megabytes of third-party package dependencies (such as node_modules, pip wheels, Maven jars, or Go modules), compiling static assets, and pre-warming build caches. Because GitHub-hosted runners run on clean, ephemeral virtual machines for every job, these dependencies are destroyed when the runner terminates unless explicitly saved.

To prevent unnecessary network bandwidth usage, reduce workflow execution duration, and optimize billable runner minutes, GitHub Actions provides a robust caching subsystem powered by actions/cache@v4 and native caching in official setup actions.


1. Mechanics of actions/cache@v4

The actions/cache action operates across two distinct phases within a job's execution lifecycle: the Restore phase (executed when the step runs) and the Save phase (executed automatically as a post-job action upon successful job completion).

+-----------------------------------------------------------------------------+
|                        ACTIONS/CACHE LIFECYCLE FLOW                         |
|                                                                             |
|   [JOB EXECUTION: STEP RUNS]                                                |
|   1. Runner queries GitHub Cache Storage for exact match on 'key'.          |
|   2. If MATCH found: Cache downloaded & extracted to 'path' -> cache-hit=true|
|   3. If NO MATCH: Runner iterates sequentially through 'restore-keys'.      |
|      - Partial prefix match found -> downloads latest cache -> cache-hit=false|
|      - No prefix match -> cache miss -> job continues fresh.                 |
|                                                                             |
|   [POST-JOB EXECUTION: JOB COMPLETES SUCCESSFULLY]                          |
|   4. If exact 'key' match occurred during restore -> SKIP SAVE (No-op).     |
|   5. If exact 'key' was NOT matched -> Archive 'path' directory & upload    |
|      new cache archive bound to 'key'.                                      |
+-----------------------------------------------------------------------------+

Core Configuration Inputs

An actions/cache configuration requires three fundamental parameters:

- name: Cache Node.js Dependencies
  id: npm-cache
  uses: actions/cache@v4
  with:
    # 1. Path: Directory or file to cache (can be multi-line for multiple paths)
    path: ~/.npm
    # 2. Key: Unique, deterministic identifier for this cache snapshot
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    # 3. Restore-Keys: Ordered prefix fallbacks when exact key is not found
    restore-keys: |
      ${{ runner.os }}-npm-

Parameter Breakdown

  1. path: The file path or directory on the runner to archive and restore. Supports absolute paths, relative paths, and glob patterns. Multiple paths can be listed on separate lines.
  2. key: A string uniquely identifying the cache. Best practice combines the runner OS (${{ runner.os }}), the ecosystem name, and a cryptographic hash of the lockfile (${{ hashFiles('**/package-lock.json') }}). If the lockfile changes, the hash changes, generating a new cache key.
  3. restore-keys: An optional list of prefix strings evaluated sequentially from top to bottom if no exact match is found for key. The runner searches for any existing cache matching the prefix and restores the most recently created one.

The cache-hit Output

The cache step sets an output variable named cache-hit. Workflows can use this output in conditional step expressions (if:) to skip redundant package installation commands when an exact match occurs:

- name: Install Dependencies
  if: steps.npm-cache.outputs.cache-hit != 'true'
  run: npm ci

[!WARNING] Partial Hits and cache-hit: If an exact key match is found, cache-hit evaluates to 'true'. However, if a partial prefix match occurs via restore-keys, cache-hit evaluates to 'false' (or is unset). This guarantees that package installation commands still run to fetch only the newly added delta dependencies.


2. Dynamic Cache Key Hashing: hashFiles()

The GitHub Actions expression function hashFiles(pattern) calculates a single SHA-256 hash across all files matching one or more comma-separated glob patterns. This enables automated cache invalidation:

Cache Key=runner.os+"-"+ecosystem+"-"+SHA256(lockfile)\text{Cache Key} = \text{runner.os} + \text{"-"} + \text{ecosystem} + \text{"-"} + \text{SHA256}(\text{lockfile})

Common hashFiles() Patterns Across Ecosystems

Ecosystem / ToolRecommended Cache PathCache Key Expression
Node.js (npm)~/.npm${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
Node.js (Yarn)~/.cache/yarn or .yarn/cache${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
Python (pip)~/.cache/pip${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/setup.py') }}
Python (Poetry)~/.cache/pypoetry${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
Java (Maven)~/.m2/repository${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
Java (Gradle)~/.gradle/caches${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
Go Modules~/go/pkg/mod${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
Rust (Cargo)~/.cargo/registry, ~/.cargo/git, target${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

[!TIP] Cache Package Manager Caches, Not node_modules: In Node.js workflows, it is strongly recommended to cache the global package cache directory (~/.npm or ~/.cache/yarn) rather than the local node_modules folder. Caching ~/.npm avoids cross-platform native binary compilation issues and allows npm ci to quickly hydrate dependencies from local disk cache safely.


3. Built-In Caching in Official Setup Actions

To simplify workflow maintenance and eliminate repetitive actions/cache boilerplate, GitHub's official setup actions provide built-in caching support via a single cache: input.

+-----------------------------------------------------------------------------+
|                 NATIVE SETUP ACTION CACHING ARCHITECTURE                    |
|                                                                             |
|   uses: actions/setup-node@v4                                               |
|   with:                                                                     |
|     node-version: '20'                                                      |
|     cache: 'npm'  <---- AUTOMATICALLY HANDLES:                              |
|                         1. Resolves path: ~/.npm                            |
|                         2. Hashes: **/package-lock.json                     |
|                         3. Generates key: ${{ runner.os }}-node-${hash}     |
|                         4. Sets fallback restore-keys: ${{ runner.os }}-node|
+-----------------------------------------------------------------------------+

Native Setup Actions Syntax Comparison

# 1. Node.js (supports 'npm', 'yarn', 'pnpm')
- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'
    cache-dependency-path: 'client/package-lock.json' # Optional for monorepos

# 2. Python (supports 'pip', 'pipenv', 'poetry')
- uses: actions/setup-python@v5
  with:
    python-version: '3.11'
    cache: 'poetry'

# 3. Go (supports go.sum caching)
- uses: actions/setup-go@v5
  with:
    go-version: '1.22'
    cache: true             # Automatically hashes go.sum

# 4. Java (supports 'maven', 'gradle', 'sbt')
- uses: actions/setup-java@v4
  with:
    distribution: 'temurin'
    java-version: '17'
    cache: 'gradle'

When using official setup actions with cache:, you do not need to declare a separate actions/cache step unless you require custom paths or specialized key naming strategies.


4. Cache Storage Limits, Eviction Policies & Branch Isolation

Understanding GitHub Actions cache quotas, retention lifecycles, and security boundaries is critical for system architecture and exam success.

Platform Storage Limits & LRU Eviction

  • 10 GB Quota per Repository: GitHub allocates a maximum of 10 GB of total cache storage per repository. There is no limit on the number of individual cache archives, provided their combined size remains under 10 GB.
  • LRU (Least Recently Used) Eviction: When a new cache upload would exceed the 10 GB repository limit, GitHub Actions automatically deletes existing caches in order of least recently accessed until total storage drops below 10 GB.
  • 7-Day Inactivity Expiration: Any cache entry that has not been accessed (read or restored) within 7 days is automatically deleted from GitHub cache storage.

Branch Cache Scope & Isolation Hierarchy

To prevent malicious cache poisoning across branches and pull requests, GitHub Actions enforces strict branch isolation boundaries:

+-----------------------------------------------------------------------------+
|                     BRANCH CACHE ISOLATION TOPOLOGY                         |
|                                                                             |
|                        +-----------------------+                            |
|                        |    DEFAULT BRANCH     |                            |
|                        |       ('main')        |                            |
|                        |  Read/Write Own Cache |                            |
|                        +-----------------------+                            |
|                                    |                                        |
|                   Child branches CAN READ from main                         |
|                                    v                                        |
|            +-----------------------------------------------+                |
|            |                                               |                |
|            v                                               v                |
|   +------------------+                           +-------------------+      |
|   |  FEATURE BRANCH  |                           |  FEATURE BRANCH   |      |
|   |    ('feat-A')    |   CANNOT READ PEER CACHE  |    ('feat-B')     |      |
|   | Read: main + A   | < - - - - - - - - - - - > |  Read: main + B   |      |
|   | Write: Only A    |                           |  Write: Only B    |      |
|   +------------------+                           +-------------------+      |
|            |                                                                |
|            v                                                                |
|   +------------------+                                                      |
|   |   PULL REQUEST   |   Read: base branch + default branch                 |
|   | (to 'feat-A')    |   Write: PR cache (isolated; deleted on PR merge)    |
|   +------------------+                                                      |
+-----------------------------------------------------------------------------+

Branch Isolation Rules Summary Matrix

Requesting Execution ScopeCan Read From?Can Write / Save To?Can Be Read By?
Default Branch (main)main cache onlymain cacheAll child branches, PRs, and workflows
Feature Branch (feat-x)feat-x cache, then fallback to main cachefeat-x cache onlyfeat-x workflows and PRs targeting feat-x
Pull Request (Internal)PR branch cache, base branch cache, main cachePR-specific temporary cacheIsolated to that PR execution scope
Pull Request (Fork)Fork repository cache only (Cannot read upstream base cache)Fork cache onlyIsolated to fork repository

[!IMPORTANT] Cache Management: Administrators can inspect, list, and manually delete repository caches using the GitHub Web UI (Actions > Caches), the REST API (DELETE /repos/{owner}/{repo}/actions/caches/{cache_id}), or the GitHub CLI (gh cache list and gh cache delete <key>).

Loading diagram...
actions/cache Restoration, Fallback Matching, and Post-Job Save Lifecycle
Test Your Knowledge

A workflow defines the following caching configuration for a Node.js project:

- name: Cache Node Modules
  id: npm-cache
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-
A developer updates a single dependency in package-lock.json on a feature branch. What occurs during the cache restoration and post-job save phases?

A
B
C
D
Test Your Knowledge

Which statement accurately describes GitHub Actions cache storage limits, eviction policies, and branch isolation rules?

A
B
C
D
Test Your Knowledge

A DevOps engineer wants to enable dependency caching for a Python project managed with Poetry using official GitHub setup actions without authoring a verbose custom actions/cache step. Which configuration is correct?

A
B
C
D