Speeding Up Workflows with Caching

Cache dependencies and build outputs across runs so CI installs in seconds instead of minutes.

Every workflow run starts on a fresh runner, so re-downloading dependencies each time is pure waste. Caching persists a folder between runs, keyed by a hash of your lockfile.

Two levels of caching

  • Built-in setup caches — actions/setup-node, setup-python, and friends take a cache: input that handles the common case for you.
  • actions/cache — the general tool for anything: build artifacts, compiler caches, Docker layers.

Getting the key right

A good key includes a hash of the lockfile, so the cache invalidates precisely when dependencies change: ${{ hashFiles('**/package-lock.json') }}. Add restore-keys as a fallback prefix so a near-miss still restores most of the cache instead of starting cold.

Example

Example · yaml
steps:
  - uses: actions/checkout@v4

  # Simple case: let setup-node manage the npm cache
  - uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: npm

  # General case: cache a custom build directory
  - uses: actions/cache@v4
    with:
      path: .next/cache
      key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.tsx') }}
      restore-keys: |
        ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-

  - run: npm ci
  - run: npm run build

When to use it

  • A Node.js team caches node_modules using the package-lock.json hash so subsequent CI runs skip npm install and save 60 seconds per run.
  • A Python project caches pip's download cache keyed to requirements.txt so dependency installation only re-runs when requirements change.
  • A monorepo uses Nx affected-only runs combined with dependency caching to reduce a 15-minute CI pipeline to under three minutes on typical PRs.

More examples

Cache npm dependencies by lockfile hash

The cache: npm option in setup-node automatically keys the node_modules cache to package-lock.json, restoring it on cache hit.

Example · yaml
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: npm          # built-in npm cache support
  - run: npm ci
  - run: npm test

Manual cache with custom key and restore

The restore-keys fallback restores the closest older cache when the exact key misses, so pip only downloads truly new packages.

Example · yaml
steps:
  - uses: actions/cache@v4
    id: pip-cache
    with:
      path: ~/.cache/pip
      key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
      restore-keys: pip-${{ runner.os }}-
  - run: pip install -r requirements.txt
  - run: pytest

Cache Gradle build outputs

Caches both the Gradle dependency cache and the wrapper binary, keyed to all Gradle files, cutting Java build times on cache hits.

Example · yaml
steps:
  - uses: actions/cache@v4
    with:
      path: |
        ~/.gradle/caches
        ~/.gradle/wrapper
      key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*','gradle-wrapper.properties') }}
  - run: ./gradlew build

Discussion

  • Be the first to comment on this lesson.