Day 4 - Speed Up CI with Caching
Optimize a slow GitHub Actions workflow by adding intelligent caching for dependencies. Achieve 40% faster run times.
Description
Your GitHub Actions workflow runs every time, but it's downloading and installing the same dependencies repeatedly. Each run takes 5+ minutes when it could take less than 2 minutes with proper caching.
Task
Add caching for dependencies to your GitHub Actions workflow.
Goal: Achieve 40% faster run times through intelligent caching.
Target
- Time Reduction: 40% or more
- Cache Hit Rate: 80%+ on subsequent runs
- Cache Size: Reasonable (under 500MB)
Sample App
Slow Workflow (Before)
name: Slow CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
# No caching - downloads every time!
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
- name: Build
run: npm run build
View Solution
Solution
Optimized Workflow (After)
name: Fast CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm' # Built-in npm caching
- name: Cache node modules
uses: actions/cache@v3
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci # Faster than npm install
- name: Cache build output
uses: actions/cache@v3
with:
path: |
dist
.next
key: ${{ runner.os }}-build-${{ hashFiles('src/**') }}
restore-keys: |
${{ runner.os }}-build-
- name: Run tests
run: npm test
- name: Build
run: npm run build
Explanation
Cache Strategy
1. Package Manager Cache
- uses: actions/setup-node@v4
with:
cache: 'npm'
Benefits:
- Caches global npm cache directory
- Automatic invalidation on package-lock.json changes
- Zero configuration
2. Node Modules Cache
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
Key Components:
- path: What to cache
- key: Unique identifier (OS + lockfile hash)
- restore-keys: Fallback if exact key doesn't match
3. Build Output Cache
- uses: actions/cache@v3
with:
path: dist
key: ${{ runner.os }}-build-${{ hashFiles('src/**') }}
Caches compiled output based on source code hash.
Cache Key Strategy
Exact Match Key
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
Creates keys like:
Linux-node-a1b2c3d4e5f6...- Changes when dependencies change
Restore Keys (Fallbacks)
restore-keys: |
${{ runner.os }}-node-
If exact key not found:
- Try most recent cache starting with
Linux-node- - Partial cache better than no cache
Performance Comparison
| Scenario | Before | After | Improvement |
|---|---|---|---|
| Cold cache | 5m 30s | 5m 00s | 9% |
| Warm cache (no changes) | 5m 30s | 1m 45s | 68% |
| Dependency change | 5m 30s | 4m 30s | 18% |
Try to solve the challenge yourself first!
Click "Reveal Solution" when you're ready to see the answer.
Result
You should see:
- ✅ 40%+ faster average build times
- ✅ Cache hits on most runs
- ✅ Reduced GitHub Actions minutes usage
- ✅ Faster developer feedback
Workflow Log Output
Cache hit for key: Linux-node-a1b2c3d4e5f6
Restored cache from: /home/runner/.npm
Cache restored successfully
Total time: 1m 47s (previously 5m 32s)
Validation
Monitor Cache Performance
Add cache statistics step:
- name: Cache stats
run: |
echo "Cache key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}"
du -sh ~/.npm 2>/dev/null || echo "No cache"
du -sh node_modules 2>/dev/null || echo "No node_modules"
Compare Run Times
# View recent workflow runs
gh run list --workflow=ci.yml --limit 10
# Compare timing
gh run view <run-id> --log | grep "Total time"
Advanced Caching Patterns
Multi-Stage Caching
# Stage 1: Dependencies
- uses: actions/cache@v3
id: cache-deps
with:
path: node_modules
key: deps-${{ hashFiles('package-lock.json') }}
# Stage 2: Build (depends on source)
- uses: actions/cache@v3
id: cache-build
if: steps.cache-deps.outputs.cache-hit == 'true'
with:
path: dist
key: build-${{ hashFiles('src/**') }}
# Only build if cache miss
- name: Build
if: steps.cache-build.outputs.cache-hit != 'true'
run: npm run build
Language-Specific Caching
Python (pip)
- uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
Go
- uses: actions/setup-go@v4
with:
go-version: '1.21'
cache: true
Docker Layers
- uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max
Cache Cleanup
- name: Clear old caches
if: github.event_name == 'schedule'
run: |
gh cache delete --all --repo ${{ github.repository }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Best Practices
✅ Do's
- Use specific cache keys: Include lockfile hashes
- Set restore-keys: Fallback to partial matches
- Cache immutable data: node_modules, build artifacts
- Monitor cache size: Keep under 500MB when possible
- Version your caches: Include version in key if needed
❌ Don'ts
- Don't cache secrets: Never cache credentials
- Don't cache .git: Checkout action handles this
- Don't use generic keys:
cache-v1is too broad - Don't cache OS files: System-specific files
- Don't ignore cache misses: Monitor and optimize
Links
Share Your Success
Optimized your CI? Share the results!
Tag @thedevopsdaily on X with:
- Before/after workflow times
- Cache hit rate percentage
- Amount of time/money saved
Use hashtags: #AdventOfDevOps #GitHubActions #Performance #Day4
Ready to complete this challenge?
Mark this challenge as complete once you've finished the task. We'll track your progress!
Completed this challenge? Share your success!
Tag @thedevopsdaily on X (Twitter) and share your learning journey with the community!
These amazing companies help us create free, high-quality DevOps content for the community
DigitalOcean
Cloud infrastructure for developers
Simple, reliable cloud computing designed for developers
DevDojo
Developer community & tools
Join a community of developers sharing knowledge and tools
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorFound an issue?