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.
Speeding Up Workflows with Caching β€” GitHub | SoundsCode