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).
- 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 testIn this example:
- path: we specify the directory to cache (
~/.npm), which is npm’s download cache. We cache this rather thannode_modules, sonpm cican install from it without hitting the network. The hash key ensures we only reuse it whenpackage-lock.jsonis 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.jsonfile. 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.
- 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@v6andactions/setup-python@v6accept acache:input (e.g.cache: npmorcache: pip) that wires up the appropriate cache automatically, so you often don’t need a separateactions/cachestep 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:
- Cache keys, restore-keys, and cache scope — the deep dive on building keys that actually hit, how
restore-keysfallback works, and the branch-scope and 10 GB eviction rules. - Caching Docker layer builds — speed up
docker buildwith Buildx layer caching (thegha,registry,inline, andlocalbackends). - Passing data between steps and jobs — including using artifacts to hand files from one job to another.
- Concurrency and cancel-in-progress — pair caching with cancelling redundant runs to avoid both slow and wasteful CI.