GitHub Actions minute usage β analysis (#832)
Generated 2026-05-28. Estimates derived from the cron cadences in
.github/workflows/ and GitHubβs per-job billing model. We do not have the
Actions billing/usage API wired up, so these are modeled figures, not billed
actuals β treat the ranking as reliable and the absolute minutes as
order-of-magnitude.
Billing model (why this matters)
GitHub Actions bills per job, and each jobβs wall-clock time is rounded up to the next whole minute. A job that does 20 seconds of real work still costs 1 minute. Consequence: high-frequency, short jobs are the most wasteful thing you can run β you pay the 1-minute floor on every single invocation, no matter how trivial the work.
Standard private-repo allowances: Free 2,000 min/mo, Pro 3,000, Team 3,000. The scheduled workflows below alone exceed all of these, which is the structural reason for the overage.
Workflow inventory
| Workflow | Trigger | Cadence | Runs/day | Runner | Notes |
|---|---|---|---|---|---|
cost-trickle-monitor.yml | schedule | 2,12,22,32,42,52 * * * * | 144 | ubuntu | cold npm install each run; no cache |
cost-spike-monitor.yml | schedule | 15 * * * * | 24 | ubuntu | cold npm install; no cache |
vendor-balance-fetcher.yml | schedule | 25 * * * * | 24 | ubuntu | cold npm install; no cache |
smoke.yml | schedule | 5 * * * * | 24 | ubuntu | npm ci on full monorepo each run |
rotation-reminder.yml | schedule | quarterly | ~0.03 | ubuntu | negligible |
deploy.yml | push main (apps/**,packages/**,some workers) + dispatch | per push | variable | ubuntu | up to 15 jobs defined |
deploy-aws.yml | push main (workers paths) + dispatch | per push | variable | ubuntu | 2 jobs |
audit.yml | PR + after every Deploy on main | per PR + per deploy | variable | ubuntu | matrix Γ2 (marketing-site, pdf-converter) |
test.yml | PR | per PR | variable | ubuntu | β |
rotate-secrets.yml | dispatch only | manual | ~0 | ubuntu | not scheduled |
Estimated scheduled-minute consumption
Assuming a realistic ~2 min billed per monitor run (VM provision + checkout +
setup-node + cold install + script) and ~5 min for the smoke (npm ci on the
whole monorepo):
| Workflow | Runs/mo | Est. min/run | Est. min/mo | Share |
|---|---|---|---|---|
| cost-trickle-monitor | 4,320 | 2 | 8,640 | ~57% |
| smoke | 720 | 5 | 3,600 | ~24% |
| cost-spike-monitor | 720 | 2 | 1,440 | ~10% |
| vendor-balance-fetcher | 720 | 2 | 1,440 | ~10% |
| rotation-reminder | ~1 | 1 | ~1 | ~0% |
| Scheduled total | ~15,100 |
Even at the absolute floor (1 min/run for monitors, 3 min for smoke) the scheduled total is ~7,900 min/mo β still 2.5β4Γ any standard allowance. Event-driven workflows (deploy, audit, test) add a variable amount on top, scaling with commit/PR volume.
cost-trickle-monitor alone is the single largest line item (~57% of
scheduled minutes) purely because it runs 144Γ/day and pays the per-job
minute floor 144 times for a few seconds of real work each time.
Structural causes (ranked)
- Per-job minute floor Γ high frequency. The trickle monitorβs 6Γ/hour cadence is the dominant cost. This is intrinsic to running sub-minute work on Actions; no amount of in-job optimization removes the floor.
- Heavy install on the smoke.
npm ciinstalls the entire monorepo every hour to run a small e2e smoke. - No dependency caching on the 3 monitors. Each cold-installs
@supabase/supabase-js. Real but secondary β caching saves seconds, the floor costs the whole minute. audit.ymlruns after every Deploy (matrix Γ2) on top of PR runs, coupling its cost to deploy frequency and always running both targets.deploy.ymlfan-out β up to 15 ubuntu jobs; if path filters/per-app conditionals donβt tightly gate them, unrelated pushes fan out.
Recommendations (prioritized by savings Γ· risk)
P0 β Move the 3 high-frequency monitors off Actions Β· ~11,500 min/mo
cost-trickle-monitor, cost-spike-monitor, and vendor-balance-fetcher are
plain .mjs scripts hitting Supabase + provider APIs. They belong on a cron
platform that runs sub-minute jobs at ~zero marginal cost with no per-minute
floor:
- Supabase
pg_cron+pg_netβ already used here (app_logs_retention,email-notify-sweep). Best fit for the Supabase-writing monitors. - Cloudflare Worker Cron Triggers β the platform already deploys Workers; good fit if the script needs npm deps / longer runtime than pg_net allows.
Same cadence, same coverage β only the execution location changes. Caveat: requires relocating the relevant secrets to the new platform, so this needs sign-off and a careful secrets move.
P1 β If monitors stay on Actions
- Halve trickle cadence (
2,22,42= 3Γ/hr) β ~4,300 min/mo saved. Changes detection latency (~5β10 min β ~10β20 min) β needs sign-off. - Add dependency caching to the 3 monitors. Low risk, marginal (~seconds/run).
P2 β Smoke Β· ~1,800β2,400 min/mo
- Install only the
@accessible-pdf/e2e-smokeworkspace instead ofnpm cion the full monorepo, or cache aggressively. - Consider every 2β3h instead of hourly, or move it to a Cloudflare Cron + alert like the monitors. Cadence change needs sign-off.
P3 β audit / deploy
- After a Deploy, run only the audit matrix target relevant to what shipped (not always both marketing-site + pdf-converter).
- Verify
deploy.ymlpath filters + per-app conditionals actually gate the 15 jobs so unrelated changes donβt fan out.
Bottom line
The overage is driven by scheduled monitors paying the per-job minute floor
at high frequency, not by CI on PRs. Moving the three monitors to Supabase
pg_cron / Cloudflare Cron (P0) removes ~75% of scheduled minutes by itself
and is the highest-leverage change; trimming the smoke install/cadence (P2) is
the next-largest. The remaining items are smaller polish.