Back to Blog
Lesson 24 of the CI/CD: Continuous Integration from Scratch course
DevOpsJuly 30, 20264 min read

Advanced Pipeline Caching: Optimizing CI/CD Performance

Learn how to use actions/cache to slash your CI/CD build times. We cover dependency caching from first principles to help you optimize pipeline performance today.

ci/cdgithub actionsoptimizationperformancedevopscaching
Large pipelines meandering through a colorful autumn forest, showcasing sustainable energy transport.

Previously in this course, we covered managing secrets and handling environment variables to secure our infrastructure. While those lessons ensure our pipeline is safe, this lesson focuses on speed: we'll implement caching to avoid downloading the same heavy dependencies every time your pipeline runs.

The Problem: Why Re-Download Everything?

Every time a GitHub Actions runner starts, it begins with a clean environment. If your project has a node_modules folder or a Python venv with hundreds of packages, your pipeline spends the first few minutes just downloading the internet.

In a professional development environment, this is wasted time and wasted bandwidth. If you run your CI pipeline 20 times a day, those 3 minutes of download time add up to an hour of developer waiting time per week.

Caching allows us to save specific directories or files across different workflow runs. If the dependency list (like package-lock.json or requirements.txt) hasn't changed, we simply restore the files from the GitHub cache server.

How Caching Works: The First Principles

Caching in CI follows a "Check, Restore, Save" lifecycle:

  1. Key Generation: We create a unique key based on the lockfile (the file that defines your exact dependency versions).
  2. Restore: The runner checks if a cache exists for that key. If it does, it downloads and extracts the files.
  3. Execution: The build runs, using the cached dependencies.
  4. Save: If the build finishes successfully, GitHub saves the specified directory back to the cache, indexed by your key.

This mechanism is similar to how we manage data in complex systems, much like how one might approach Redis memory optimization to keep frequently accessed data close at hand.

Worked Example: Caching NPM Dependencies

Let’s optimize a Node.js-based project. We use the official actions/cache action to target the node_modules folder.

YAML
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Cache node modules
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

Key takeaways from this configuration:

  • path: The location on the runner where dependencies live.
  • key: A unique string. We use hashFiles so that if package-lock.json changes, the key changes, forcing a fresh cache.
  • restore-keys: A fallback. If the exact key isn't found, it looks for any cache starting with the prefix, which is faster than starting from zero.

Hands-on Exercise

  1. Open your existing workflow file.
  2. Identify your primary dependency manager (e.g., npm, pip, poetry).
  3. Add the actions/cache step before your npm install or pip install step.
  4. Run your workflow and check the "Actions" tab in your repository.
  5. Notice the difference in "Install dependencies" step duration between the first run (Cache Miss) and the second run (Cache Hit).

Common Pitfalls

  • Ignoring the Lockfile: Never cache based on the project name alone. Always hash the lockfile. If you don't, the cache will become stale, and your build will test against outdated dependencies.
  • Over-caching: Don't cache temporary build outputs (like dist/ or build/ folders) that are meant to be fresh every time. Cache only stable external dependencies.
  • Cache Eviction: GitHub automatically evicts caches that haven't been accessed in 7 days or if the total cache size exceeds 10GB. Don't treat the cache as permanent storage.

FAQ

Does caching make every build faster? The first build will be slower (because it has to save the cache). Subsequent builds will be significantly faster.

What happens if the cache is corrupted? You can trigger a fresh build by modifying a comment in your lockfile or by manually clearing the cache in the GitHub UI under "Settings > Actions > Caches".

Can I share caches across different workflows? Yes, as long as they are in the same repository and use the same key prefix.

Recap

Performance optimization is a core DevOps skill. By implementing actions/cache, we’ve moved from "re-download everything" to "only download what’s new." This simple pattern is essential for maintaining developer velocity in larger projects, similar to how we manage Advanced Cache Invalidation in application states.

Up next: We will learn how to persist build outputs—not just dependencies—using Artifact Management.

Similar Posts