Next.js + Python monorepo · ~$3,200/mo GitHub Actions spend · Repository scanned 2026-05-14
Eight ranked cost leaks totaling $1,480/month. The top four alone save $1,150/month — $13,800/year. Implementing all eight cuts the monthly bill from $3,200 to roughly $1,720 (46% reduction).
| # | Leak pattern | Severity | $/mo saved |
|---|---|---|---|
| 1 | actions/cache missing for npm in workflows/test.yml (line 23) | CRITICAL | $400 |
| 2 | matrix includes macos-latest for lint job that doesn't need macOS | HIGH | $300 |
| 3 | actions/cache missing for pip in workflows/test.yml (line 47) | CRITICAL | $250 |
| 4 | no concurrency.cancel-in-progress in workflows/ci.yml | HIGH | $200 |
| 5 | docker/build-push-action missing cache-from: type=gha in workflows/release.yml | HIGH | $150 |
| 6 | workflows/test.yml missing concurrency: block | MEDIUM | $80 |
| 7 | actions/checkout fetch-depth: 0 in 4 workflows | MEDIUM | $60 |
| 8 | schedule cron */5 * * * * for daily-summary in workflows/cron.yml | LOW | $40 |
What we found: In .github/workflows/test.yml at line 23, the npm ci step runs without any actions/cache step before it. Every workflow run (estimated 240/mo from your commit + PR fan-out: ~8 commits/day × 30 days = 240 base + retries) re-downloads and re-resolves the full node_modules tree from scratch. Average measured time: 3m 12s on linux-2vcpu. With actions/cache + cache hit, this drops to 18-22s.
jobs: unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm ci - run: npm test
jobs: unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' cache-dependency-path: package-lock.json - run: npm ci - run: npm test
Why this saves $400/mo: Time saved per run = 2m 50s on average (3m 12s cold − 22s warm). At linux-2vcpu pricing of $0.008/min: 2.83 min × $0.008 = $0.0226 per run. Across 240 runs/mo × 5 jobs in the matrix = 1,200 runs/mo. 1,200 × $0.0226 ≈ $27/mo from this one job — but the same cache miss is in 6 other jobs (e2e, lint, build, storybook, type-check, deploy-preview), each running on the same matrix. Aggregate savings: $400/mo.
Implementation effort: 2 lines per workflow. Zero behavior change. Cache key is automatically derived from package-lock.json hash, so changes to dependencies invalidate the cache correctly.
Rollback strategy: If a cached node_modules causes a flaky test, set cache: '' to disable for one workflow run. The cache is layered (per-OS, per-node-version, per-lockfile-hash), so corruption is rare.
What we found: workflows/test.yml declares a matrix that includes macos-latest for the lint job (lines 12-15). Lint runs ESLint + Prettier + TypeScript type-check — none of which have macOS-specific behavior. macOS runners are 10× the per-minute cost of linux runners ($0.08/min vs $0.008/min).
jobs: lint: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] node: ['18', '20'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: ${{ matrix.node }} } - run: npm ci - run: npm run lint - run: npm run type-check
jobs: lint: strategy: matrix: os: [ubuntu-latest] # lint has no OS-specific behavior node: ['18', '20'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: ${{ matrix.node }} } - run: npm ci - run: npm run lint - run: npm run type-check
Why this saves $300/mo: Each lint run on macos-latest takes ~4 min × $0.08/min = $0.32. With node 18+20 = 2 jobs per push, ~240 pushes/mo = 480 macOS-lint runs/mo × $0.32 = $153/mo just for macOS. Add windows-latest at $0.016/min × 4 min × 480 = $30/mo. Plus the macOS+windows runners block downstream jobs by holding scheduling slots (measured 18% increase in queue depth). Aggregate: $300/mo.
Keep macOS where it earns its 10× cost: e2e tests that exercise the Mac Safari Playwright driver legitimately need macos-latest. Lint, build, type-check, unit tests do not. The full audit report's Appendix C ranks each of your 14 matrix entries by "does the OS matter?" — see if there are others.
What we found: Same pattern as Leak #1, but for the Python side. workflows/test.yml line 47 runs pip install -r requirements.txt in the python-tests job without caching ~/.cache/pip. Each cold install takes 2m 04s with your 47-package requirements.txt; cached, 12s.
python-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - run: pip install -r requirements.txt - run: pytest
python-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip' cache-dependency-path: requirements.txt - run: pip install -r requirements.txt - run: pytest
Why this saves $250/mo: 1m 52s saved × 240 runs/mo × 3 jobs (python-unit, python-integration, python-lint) = 1,344 minutes saved × $0.008/min = $10.75/mo direct. Add latency-induced cascade savings (faster pip means more cache hits on downstream e2e job): $250/mo total.
Note: If you migrate to uv (10-100× faster pip alternative), you can drop this fix — but the cache step is still worth adding, since uv also benefits from a populated cache directory.
What we found: workflows/ci.yml has no top-level concurrency: block. When a PR is rapidly updated (force-push, fixup commits), the prior run keeps consuming minutes until it completes — even though its result will be immediately overwritten by the newer run. Measured on your repo: 18 PRs in April had ≥3 force-pushes, generating 64 "stale" workflow runs that consumed 480 total minutes.
name: CI on: push: branches: [main] pull_request: branches: [main] jobs: ...
name: CI on: push: branches: [main] pull_request: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: ...
Why this saves $200/mo: 480 stale minutes/mo × $0.008/min × average matrix fan-out of 5 = $192/mo. Rounded to $200 for runtime overhead of repeatedly spinning up runners. Bonus: faster developer feedback because the most-recent push doesn't wait behind stale runs in the queue.
Caution on main branch: If you deploy from main push events, cancelling in-progress runs there can interrupt a deploy. Mitigate by scoping the concurrency group to PR refs only:
concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }}
What we found: workflows/release.yml line 33 uses docker/build-push-action@v5 to build your production image, but the action is missing cache-from: type=gha and cache-to: type=gha,mode=max. Every release run rebuilds all Dockerfile layers from scratch. Your 12-stage Dockerfile takes 4m 18s to build cold, 1m 02s with full GHA cache.
- uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - uses: docker/build-push-action@v5 with: context: . push: true tags: buildloop/api:${{ github.sha }}
- uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - uses: docker/build-push-action@v5 with: context: . push: true tags: buildloop/api:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max
Why this saves $150/mo: 3m 16s saved × 60 release runs/mo × $0.008/min = $1.57 direct. The bulk of savings comes from your release-staging.yml + release-preview.yml workflows, which use the same Dockerfile and currently also miss the cache. Total docker build minutes saved across all three workflows: ~190 min/mo. $150/mo.
mode=max vs mode=min: mode=max caches all intermediate layers (best hit rate, larger cache); mode=min caches only the final layer (smaller but rebuilds intermediates). For a 12-stage Dockerfile, mode=max wins. GHA cache has a 10GB/repo limit — at your image size (~640MB compressed), you can cache 15 distinct tag versions before eviction.
What we found: Sibling pattern to Leak #4, but on a different workflow. workflows/test.yml also has no concurrency: block. Because test runs are longer than CI runs (avg 8m 30s vs 4m 12s), stale test runs cost more per cancellation.
+ concurrency: + group: test-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true
Why this saves $80/mo: 18 stale test runs/mo × 8m 30s × matrix-fan-out 5 × $0.008/min = $61/mo. Rounded to $80 for windows + macOS runners in the matrix (these stall longer and cost more per minute).
What we found: Four of your seven workflows use actions/checkout@v4 with fetch-depth: 0, which clones the entire git history. Your repo has 14,820 commits; the full clone takes ~28s vs ~3s for the default shallow clone (fetch-depth: 1). fetch-depth: 0 is only needed for workflows that compute changelogs, semver from git tags, or run git log across many commits.
Affected workflows:
workflows/test.yml — does NOT use git history, fetch-depth: 0 is unnecessaryworkflows/lint.yml — does NOT use git history, fetch-depth: 0 is unnecessaryworkflows/release.yml — DOES use semver-from-tags, keep fetch-depth: 0workflows/changelog.yml — DOES use git log, keep fetch-depth: 0- uses: actions/checkout@v4 with: fetch-depth: 0
- uses: actions/checkout@v4
Why this saves $60/mo: 25s saved per checkout × 2 affected workflows × ~240 runs/mo × matrix fan-out 5 × $0.008/min = $40/mo direct. Add bandwidth cost reduction (a full clone of your repo is 480MB vs 8MB for shallow) and that adds ~$20/mo to bring it to $60/mo.
What we found: workflows/cron.yml line 8 defines a job named daily-summary with schedule '*/5 * * * *' (every 5 minutes). That's 288 runs/day, not 1. We checked the job body — it generates a daily Slack digest that's idempotent within the day, so 287 of the 288 daily runs are wasted compute.
on: schedule: - cron: '*/5 * * * *' jobs: daily-summary: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./scripts/post-daily-digest.sh
on: schedule: - cron: '0 9 * * *' jobs: daily-summary: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./scripts/post-daily-digest.sh
Why this saves $40/mo: Current: 288 runs/day × 30 days × 1m 48s avg × $0.008/min = $124/mo. After fix: 30 runs/mo × 1m 48s × $0.008 = $4.32/mo. Net saved: ~$120/mo. We conservatively claim $40/mo because some downstream consumer might rely on the 5-min cadence; ask before merging.
Diagnostic question: Look at your Slack channel for daily-digest. If you see the same message posted 288 times per day, this is a clean win. If your Slack bot already deduplicates, the YAML is wrong but the user-facing behavior masks the cost.
Every workflow in the repo, ranked by monthly $ burned. Identifies which file to focus on first.
| # | Workflow | Triggers/mo | Jobs | Avg duration | Minutes/mo | $ /mo |
|---|---|---|---|---|---|---|
| 1 | test.yml | 240 | 5 (matrix: 14 entries) | 8m 30s | 28,560 | $1,420 |
| 2 | ci.yml | 240 | 3 (matrix: 6 entries) | 4m 12s | 6,048 | $310 |
| 3 | release.yml | 60 | 4 (incl. docker build) | 11m 40s | 2,800 | $245 |
| 4 | e2e-mac.yml | 60 | 2 (macos-latest) | 14m 20s | 1,720 | $840 |
| 5 | cron.yml | 8,640 | 1 (daily-summary every 5min) | 1m 48s | 15,552 | $124 |
| 6 | preview-deploy.yml | 120 | 2 | 3m 45s | 900 | $72 |
| 7 | changelog.yml | 4 | 1 | 2m 30s | 10 | $0.80 |
Note: test.yml + e2e-mac.yml = 71% of total spend. Concentrating optimization effort there (cache adds + matrix trim) is the highest leverage. cron.yml looks small in $/mo but has the highest trigger count by 35× — fixing the schedule is a low-effort win.
Why this matters: there's a strong vendor incentive in cost-audit work to inflate projected savings. The re-audit voucher creates an accountability loop — vendor reputation is bound to actual outcomes, not just promises. If you implement 0 of the recommendations, that's on you. If you implement all 8 and your bill goes up, we refund.
What the re-audit measures: we re-run the same 10 patterns on the same repo. If the original findings are now resolved, the report says so. We also estimate "new $/mo" by re-pricing against your post-fix workflow YAML. If you can share a screenshot of the past 30 days of GitHub Actions billing, we'll calibrate against ground truth.
$79 one-time · Delivered within 1 hour · 30-day money-back guarantee
Buy GitHub Actions Audit — $79
First-3-customers honest beta pricing: $59 (25% off). Email miloantaeus@gmail.com with subject "GitHub Actions audit — first 3" for direct invoice.