How do I cache steps in GitHub actions?

Most use-cases are covered by existing actions, for example:

  • actions/setup-node for JS
  • docker/build-push-action for Docker

Custom caching is supported via the cache action. It works across both jobs and workflows within a repository. See also: GitHub docs and Examples.

Consider the following example:

name: GitHub Actions Workflow with NPM cache

on: push

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Cache NPM dependencies
      uses: actions/cache@v3
      with:
        path: ~/.npm
        key: ${{ runner.OS }}-npm-cache-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ runner.OS }}-npm-cache-

    - name: Install NPM dependencies
      run: npm install

How caching works step-by-step:

  • At the Cache NPM dependencies step, the action will check if there's an existing cache for the current key
  • If no cache is found, it will check spatial matches using restore-keys. In this case, if package-lock.json changes, it will fall back to a previous cache. It is useful to prefix keys and restore keys with the OS and name of the cache, as it shouldn't load files for a different type of cache or OS.
  • If any cache is found, it will load the files to path
  • The CI continues to the next step and can use the filed loaded from the cache. In this case, npm install will use the files in ~/.npm to save downloading them over the network (note that for NPM, caching node_modules directly is not recommended).
  • At the end of the CI run a post-action is executed to save the updated cache in case the key changes. This is not explicitly defined in the workflow, rather it is built into the cache action to take care of both loading and saving the cache.

You can also build your own reusable caching logic with @actions/cache such as:

  • 1-liner NPM cache
  • 1-liner Yarn cache

Old answer:

Native caching is not currently possible, expected to be implemented by mid-November 2019.

You can use artifacts (1, 2) to move directories between jobs (within 1 workflow) as proposed on the GH Community board. This, however, doesn't work across workflows.


The cache action can only cache the contents of a folder. So if there is such a folder, you may win some time by caching it.

For instance, if you use some imaginary package-installer (like Python's pip or virtualenv, or NodeJS' npm, or anything else that puts its files into a folder), you can win some time by doing it like this:

    - uses: actions/cache@v2
      id: cache-packages  # give it a name for checking the cache hit-or-not
      with:
        path: ./packages/  # what we cache: the folder
        key: ${{ runner.os }}-packages-${{ hashFiles('**/packages*.txt') }}
        restore-keys: |
          ${{ runner.os }}-packages-
    - run: package-installer packages.txt
      if: steps.cache-packages.outputs.cache-hit != 'true'

So what's important here:

  1. We give this step a name, cache-packages
  2. Later, we use this name for conditional execution: if, steps.cache-packages.outputs.cache-hit != 'true'
  3. Give the cache action a path to the folder you want to cache: ./packages/
  4. Cache key: something that depends on the hash of your input files. That is, if any packages.txt file changes, the cache will be rebuilt.
  5. The second step, package installer, will only be run if there was no cache

For users of virtualenv: if you need to activate some shell environment, you have to do it in every step. Like this:

- run: . ./environment/activate && command