CI runs too often
Scheduled CI jobs that don’t earn their keep
By Keith Mazanec, Founder, CostOps ยท Updated February 8, 2026
Why are scheduled CI workflows wasteful?
Scheduled GitHub Actions workflows (cron jobs) run on a fixed timer regardless of code changes. A typical nightly build runs 30 times a month but catches a real issue only twice a year. Most teams can cut scheduled CI spend 50–80% by adding a change-detection gate that skips runs when nothing changed, reducing frequency from daily to weekly, or replacing cron triggers with event-driven paths: filters on push.
Your nightly build runs 30 times a month. It catches a real issue twice a year. The other 348 runs produce no signal, but you pay for every one. Cron-triggered workflows are the quietest line item on your CI bill because they run in the background, nobody reviews them, and the spend compounds silently.
Symptoms
How to tell if your scheduled workflows are wasting money
Open your repository’s Actions tab and filter by event:schedule. The patterns are easy to spot once you know what to look for:
-
High minutes from scheduled runs. Check what percentage of your total Actions minutes come from schedule-triggered workflows. If it’s over 20%, scheduled jobs are a meaningful cost driver, and unlike PR-triggered runs, they aren’t directly tied to developer productivity.
-
Near-zero failure rate. A nightly workflow that passes 99% of the time isn’t catching bugs. It’s confirming what you already know. If the failure rate is below 2%, the job is producing almost no actionable signal for its cost.
-
Runs on days with no code changes. Your cron job runs every night. On weekends, holidays, and slow weeks, nothing changed since the last run. The workflow re-validates the same SHA it already validated, burning minutes for identical results.
-
Failures nobody acts on. The nightly build failed last Tuesday. Nobody noticed until Thursday. If scheduled workflow failures don’t trigger an immediate response, the job isn’t serving as a safety net. It’s just generating noise.
Metrics
How much do scheduled workflows cost on GitHub Actions?
Three daily scheduled workflows across 4 private repos on standard Linux runners ($0.006/min) cost roughly $39/month in billed minutes. Reducing to 30% of those runs saves $28/month. Here’s the breakdown for a typical setup: a nightly build, a daily security scan, and a daily dependency check:
Before optimization
At $0.006/min (Linux 2-core)
After optimization (70% fewer runs)
Save $28/mo · $336/year · across 4 repos
That’s Linux. If any of those scheduled jobs run on macOS at $0.062/min, the before number jumps to $402/mo and the savings hit $285/mo. Free-tier teams on the GitHub Free plan get 2,000 minutes/month, and a single nightly build consuming 18 minutes eats 540 of those minutes, over a quarter of your entire allocation, before a single PR runs.
Fix 1
How to skip scheduled runs when nothing changed
The most direct fix: don’t run the build if no commits have landed since the last run. Add a check step at the top of your scheduled workflow that compares the current HEAD SHA against the last successful run. If the code hasn’t changed, exit early. You’ll burn 1 minute on the check step instead of 18 minutes on the full build.
name: Nightly Build on: schedule: - cron: '30 2 * * 1-5' # Weekdays at 02:30 UTC jobs: check-changes: runs-on: ubuntu-latest outputs: has_changes: ${{ steps.check.outputs.has_changes }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check for recent commits id: check run: | COMMITS=$(git log --oneline --since="25 hours ago" | wc -l) if [ "$COMMITS" -gt 0 ]; then echo "has_changes=true" >> $GITHUB_OUTPUT else echo "has_changes=false" >> $GITHUB_OUTPUT echo "No commits in the last 25 hours. Skipping." fi build: needs: check-changes if: needs.check-changes.outputs.has_changes == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm test - run: npm run build
The 25 hours ago window (instead of 24) gives a buffer for schedule delays. GitHub warns that scheduled events can be delayed during periods of high load, especially at the top of the hour. The check-changes job costs about 1 billed minute regardless, but that’s 17 minutes saved on every no-change day.
Fix 2
Should you run scheduled CI daily or weekly?
Weekly is enough for most scheduled CI jobs. Dependency audits, license checks, and full integration suites rarely need daily runs. Most scheduled workflows run daily only because someone picked 0 0 * * * when they set it up and nobody revisited the frequency. Ask: if this job catches a problem, does it matter whether you find out at midnight or on Monday morning?
on: schedule: - cron: '0 0 * * *' # Every day
on: schedule: - cron: '30 3 * * 1' # Mondays at 03:30 UTC
Going from daily to weekly is an 85% reduction in runs. For an 18-minute job on Linux, that’s 540 → 78 minutes/month, saving 462 minutes per workflow. Also avoid scheduling at the top of the hour (0 * * * *) because GitHub Actions experiences peak load at :00 and your job may be delayed or dropped entirely. Use an offset like :30 or :15.
One caveat: scheduled workflows only run on the default branch. If you need to validate a long-lived release branch, you can’t use schedule directly, so use workflow_dispatch triggered by an external cron instead.
Fix 3
How to replace cron with event-driven triggers
Use the push event with paths: filters instead of a cron schedule. The underlying need behind most scheduled workflows is “run this when something changes,” which GitHub Actions handles natively. Dependency audits should run when lockfiles change. Security scans should run on push to main. Integration tests should run after deploys.
name: Security Audit # Before: ran every night regardless of changes # on: # schedule: # - cron: '0 0 * * *' # After: runs only when dependencies actually change on: push: branches: [main] paths: - 'package-lock.json' - 'yarn.lock' - 'pnpm-lock.yaml' - 'Gemfile.lock' - 'requirements.txt' - 'go.sum' schedule: - cron: '30 3 * * 1' # Weekly fallback for new CVEs workflow_dispatch: # Manual trigger for on-demand runs jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm audit --audit-level=high
This pattern gives you three triggers: immediate feedback when lockfiles change (using path filter patterns), a weekly safety net for newly disclosed CVEs, and a manual button for on-demand runs. The daily cron is gone. On a repo that merges 3–5 lockfile changes per month, this drops from 30 runs/month to roughly 8.
For dependency scanning specifically: check whether GitHub’s built-in Dependabot already covers your use case. Dependabot alerts are free on all plans and scan every push automatically. If you’re running npm audit or pip-audit on a cron because you set it up before Dependabot existed, you may be paying for a duplicate of a free feature.
Fix 4
What is workflow_dispatch and when should you use it?
The workflow_dispatch event adds a “Run workflow” button to the GitHub Actions tab, with optional input parameters. Use it to replace daily crons for jobs that only need to run when someone actually wants the results. Pair it with a weekly schedule fallback to keep a safety net without the daily cost.
name: Full Integration Suite on: workflow_dispatch: inputs: environment: description: 'Target environment' required: true default: 'staging' type: choice options: - staging - production schedule: - cron: '0 4 * * 1' # Weekly Monday fallback jobs: integration: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./scripts/integration-tests.sh ${{ inputs.environment || 'staging' }}
This keeps a weekly backstop while giving the team the ability to trigger a run on demand. If the team runs it manually 2–3 times a week when they need it, that’s still fewer runs than a daily cron, and every run produces results someone actually wants.
Reference
How to audit your scheduled workflows
Run this checklist against every schedule-triggered workflow in your organization. For each one, decide: keep, reduce, replace, or remove.
| Question | Action |
|---|---|
| Failure rate < 2% over 90 days? | Reduce to weekly or remove entirely |
| Duplicates a built-in feature? | Remove and use Dependabot, CodeQL, or secret scanning instead |
| Could trigger on file changes instead? | Replace cron with paths: filter on push |
| Runs on days with no commits? | Add a change-detection gate (Fix 1) |
| Team runs it manually when needed? | Switch to workflow_dispatch with weekly fallback |
| Failures go unnoticed for days? | Fix alerting first, or remove the job |
One more thing: GitHub automatically disables scheduled workflows on public repositories after 60 days of no repository activity. For private repos, there’s no auto-disable, so orphaned cron jobs in inactive private repos run forever, silently consuming your minutes allocation.
Related guides
Optimize CodeQL and Security Scans
Move full scans to main, use diff-aware tools on PRs, and cut security CI spend 60-80%.
Stop CI Running on Every Push
Use concurrency groups, scoped triggers, and path filters to cut redundant CI runs.
Control Dependabot and Renovate CI Costs
Use actor filtering and lightweight workflows to cut bot CI spend 50-70%.
Set Timeout-Minutes to Stop Paying for Hung Jobs
Cap wasted CI spend with explicit timeout-minutes on every job and step.