self-host →

Using caching to speed up GitHub Actions workflows

Learn how to use caching in GitHub Actions to speed up your workflows by caching dependencies and other frequently reused files.

As we continue to refine our GitHub Actions workflows, it’s essential to look for optimizations that can save time and resources. One feature at our disposal is caching. By caching dependencies and other frequently reused files, we can significantly reduce the time our workflows take to run.

Understanding caching in GitHub Actions#

Caching works by storing a copy of specific files or directories, like your project’s dependencies, between workflow runs. When your workflow runs again, it can reuse the cached files instead of regenerating or downloading them, which can often be time-consuming.

Why Caching?

  • Faster workflows: reusing previously downloaded or compiled files can drastically reduce build and setup times.
  • Reduced network latency: by avoiding repeated downloads, you also reduce potential network-related delays.
  • Improved reliability: minimizing external network requests decreases the chance of a failure due to network issues.

Implementing caching in your workflows#

GitHub Actions provides a built-in cache action that you can use to cache dependencies and other files. The key to effective caching is identifying what to cache and how to invalidate the cache when necessary (for example, when dependencies change).

  1. Basic caching example

Here’s how to cache Node.js dependencies using the cache action in a GitHub Actions workflow:

name: Node.js CI
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Cache Node modules
uses: actions/cache@v5
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
run: npm ci
- name: Test
run: npm test

In this example:

  • path: we specify the directory to cache (~/.npm), which is npm’s download cache. We cache this rather than node_modules, so npm ci can install from it without hitting the network. The hash key ensures we only reuse it when package-lock.json is unchanged.
  • key: a unique key that represents the specific cache instance. We use a combination of the runner OS and a hash of the package-lock.json file. The hash changes if the dependencies change, creating a new cache entry for the next run.
  • restore-keys: used to restore from partially matching cache keys if there’s no exact match on the key.
  1. Caching across different environments and languages

Caching is not limited to Node.js or npm. You can implement similar strategies for other environments and dependency managers (e.g., Python with pip, Ruby with Bundler, etc.) by adjusting the path and key to match those environments’ specifics.

For example, here’s the same pattern applied to Python’s pip cache:

- name: Cache pip
uses: actions/cache@v5
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-

Tip: many setup-* actions can manage this for you. For instance, actions/setup-node@v6 and actions/setup-python@v6 accept a cache: input (e.g. cache: npm or cache: pip) that wires up the appropriate cache automatically, so you often don’t need a separate actions/cache step at all.

Best practices for caching#

  • Be specific with cache keys: use precise keys to avoid restoring the wrong cache. Including the platform, language version, and a hash of dependency files in the cache key is a good practice.
  • Cache dependencies, not build outputs: cache items that take a long time to download or generate but change infrequently. Avoid caching build outputs which can vary significantly between runs.
  • Understand cache limits and eviction: GitHub gives each repository a default cache budget of 10 GB. Once that limit is exceeded, caches are evicted by last-access date (least recently used first) until the repository is back under the limit. On top of that, any cache entry that hasn’t been accessed in 7 days is removed automatically. Be mindful of what you cache to stay within these limits, or switch to another cache backend or GitHub Action providers that come with much higher cache sizes.
  • Know how cache scope works: caches are scoped by branch. A workflow run can read caches created on its own branch, on the repository’s default branch, and (for pull requests) on the base branch — but it cannot restore caches from unrelated sibling or child branches. This is why a fresh feature branch often gets a cache miss until the default branch’s cache has been populated.

Conclusion#

Caching is a powerful tool in your GitHub Actions workflows, enabling faster builds, reducing network dependency, and making your CI/CD process more efficient. This article focused on the basics of dependency caching. From here: