
Jenkins to GitHub Actions Migration Guide
Your GitHub Actions bill last month was higher than you expected. You're not alone. Most teams run every job on default 2-core Linux runners, leave caches misconfigured, and never cancel redundant workflow runs. The result is a monthly bill that grows linearly with team size while delivering diminishing returns.
This guide covers the full optimization surface: per-minute pricing mechanics, runner right-sizing, cache hit rate improvements, concurrency controls, workflow auditing, and where third-party runners like Tenki change the economics. Each section includes specific numbers and YAML you can ship this week.
GitHub bills Actions compute per minute, rounded up to the nearest whole minute for each job. Public repositories get free standard runner usage. Private repositories get a monthly allowance based on your plan: 2,000 minutes on Free, 3,000 on Pro and Team, and 50,000 on Enterprise Cloud.
Once you exceed your included minutes, every runner minute costs real money. And the cost varies dramatically by runner type.
Here's what GitHub charges per minute for standard hosted runners:
ubuntu-latest runner and where most spend concentrates.A team running 10,000 minutes/month on default Linux runners spends $60. The same workload on macOS? $620. That single choice is the biggest cost lever most teams ignore.
GitHub's larger runners (available on Team and Enterprise plans) scale roughly linearly in price but not always linearly in performance. The key rates for Linux x64:
ARM64 runners are substantially cheaper at every tier. A 4-core arm64 runner costs $0.008/min versus $0.012/min for x64. That's a 33% discount for workloads that can run on ARM.
One detail teams miss: larger runners don't draw from your included minutes. Every minute is billed at the per-minute rate, even on public repositories.
The default 2-core runner is the right choice for most jobs. But not all jobs. The mistake teams make is applying it universally without measuring.
A 4-core runner costs 2x per minute ($0.012 vs $0.006). But if your build is CPU-bound and parallelizable, the 4-core runner might finish in 55% of the time. You'd pay 2x the rate for 55% of the minutes, netting a 10% cost increase for a 45% faster build. Whether that trade-off is worth it depends on your feedback loop requirements.
For single-threaded jobs (most linting, simple test suites, deployment scripts), extra cores sit idle. You're paying double for nothing.
Bigger runners actually reduce total cost when the speedup exceeds the price multiplier. This happens with:
The measurement approach is straightforward: run the same job on both runner sizes for a week, compare total billed minutes, and multiply by the per-minute rate. If the larger runner's total cost is lower, switch.
macOS runners cost $0.062/min for 3-4 cores, $0.077/min for 12-core M1, and $0.102/min for 5-core M2 Pro. These rates are 10-17x the equivalent Linux cost.
macOS is justified when you're building iOS/macOS apps that require Xcode, running UI tests on Apple platforms, or signing/notarizing macOS binaries. For everything else, cross-compile on Linux and run macOS-specific tests in a minimal job that takes two minutes, not twenty.
Caching is where small configuration changes produce outsized savings. Every minute your workflow spends downloading dependencies or rebuilding artifacts that haven't changed is money burned.
Aim for 85%+ cache hit rates on dependency caches. Below 70%, something is wrong with your cache key strategy. You can check your repo's cache hit rate in the Actions tab under "Caches" or via the REST API.
The most common cache key mistake is keying only on the lockfile hash. That works when the lockfile changes rarely, but breaks when you have multiple OS/Node version matrix entries that produce different native binaries for the same lockfile. Add the runner OS and runtime version to your cache key.
Not all caches deliver equal value. Here's how they rank by impact:
node_modules directly. The setup-node action handles this correctly when you set cache: 'npm'.type=gha) with docker/build-push-action. This can cut Docker build times by 60-80% on subsequent runs.The restore-keys parameter in actions/cache is your fallback hierarchy. If the exact key misses, GitHub tries prefix matches. A good pattern for Node projects:
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-The restore key prefix means even when the lockfile changes, you'll restore the previous cache and only download the diff. Without restore-keys, every lockfile change triggers a full cold download.
GitHub gives you 10 GB of cache storage per repository for free. Beyond that, it's $0.07/GB/month. Caches that haven't been accessed in 7 days are automatically evicted. If your repo hits the limit, older caches get purged first.
Monitor your cache usage. If you have 15 matrix combinations each producing a 600 MB cache, you're at 9 GB and evicting constantly. Consolidate matrix keys or reduce what you cache.
A developer pushes to a branch, CI starts, they push again 30 seconds later with a fix, and now two workflow runs are burning minutes for the same branch. The first run is already stale. But it'll run to completion unless you tell it not to.
Add a concurrency group to your workflow and set cancel-in-progress: true:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueThis groups runs by workflow name and branch. When a new run starts for the same group, the previous run is cancelled immediately. On a team of 10 developers pushing to feature branches several times a day, this alone can cut 15-30% of total minutes.
One caveat: don't set cancel-in-progress: true on deployment workflows that target production. You want those to queue, not cancel.
The group key determines what gets cancelled. Common patterns:
${{ github.workflow }}-${{ github.ref }} groups by workflow and branch. The most common and generally the right default.${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} groups by PR number when available. Better for workflows triggered by both push and pull_request events.deploy-${{ github.ref }} for deployment workflows where you want queuing, not cancellation.Before changing runner sizes or cache configs, audit what you're actually running. Pull up your workflow files and work through these checks.
paths-ignore: ['docs/**', '*.md']Matrix strategies multiply your job count. A matrix of 3 Node versions, 3 OS targets, and 2 package managers creates 18 jobs per workflow run. Do you actually support all 18 combinations in production?
Often the answer is no. Run the full matrix on the nightly or weekly schedule. For PR checks, run the primary combination and one or two edge cases.
The default job timeout is 6 hours. A flaky integration test that hangs will burn 360 minutes before GitHub kills it. Set explicit timeouts on every job:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15Set it to 2-3x your expected job duration. If tests normally take 5 minutes, a 15-minute timeout catches hangs early without false-positiving on slow runs.
Teams accumulate workflows over time. You'll often find two or three workflows that each run npm install and npm run build independently. Consolidate into a single workflow with multiple jobs that share artifacts, or use reusable workflows to avoid duplicate setup.
All the optimizations above work within GitHub's pricing structure. But the per-minute rate itself is a variable you can change by switching runner providers.
Tenki Runners charges $0.002/core/minute for x64 Linux runners. Here's how that compares to GitHub for common configurations:
The per-minute savings aren't the whole story though. Tenki runs on bare-metal servers, which means faster I/O and no noisy-neighbor performance variance. Teams consistently report 30% faster build times on Tenki compared to GitHub-hosted runners for the same workflow. Faster builds mean fewer billed minutes, which compounds on top of the lower rate.
Migration is minimal. Tenki works with your existing GitHub Actions workflows. You change the runs-on label, and Tenki even opens an automated PR to handle the migration. No YAML rewrite, no new CI system to learn.
Tenki's free Starter plan includes $10 in monthly credits, up to 5 concurrent runner jobs, and runners up to 4 cores with 8 GB RAM. That's enough to trial on a real workload before committing. The Team plan ($200/month) includes $100 in credits, up to 50 concurrent jobs, and runners up to 64 cores.
For a team spending $500/month on GitHub Actions, moving Linux workloads to Tenki could bring that closer to $300/month from the rate difference alone, before accounting for speed improvements.
Cutting costs is one thing. Keeping them cut requires governance.
GitHub lets you set budgets on metered products and sends email alerts at 90% and 100% of your included usage. On Enterprise plans, you can set spending limits that block usage once exceeded.
Set a budget 20% above your expected monthly spend. This gives headroom for legitimate spikes (release weeks, large PRs) while catching runaway workflows before they burn through your quarterly allocation.
For cost center reporting, track these metrics monthly:
Beyond GitHub's built-in alerts, set up a scheduled workflow that queries the billing API daily and posts to Slack when the run rate exceeds your budget threshold:
name: Billing Alert
on:
schedule:
- cron: '0 9 * * 1-5'
jobs:
check-spend:
runs-on: ubuntu-latest
steps:
- name: Check Actions usage
run: |
USAGE=$(gh api /orgs/$ORG/settings/billing/actions \
--jq '.total_minutes_used')
echo "Minutes used this cycle: $USAGE"
if [ "$USAGE" -gt 40000 ]; then
curl -X POST "$SLACK_WEBHOOK" \
-d '{"text": "Actions usage alert: '"$USAGE"' minutes used"}'
fiRun this on weekday mornings so you catch problems before they compound over a weekend.
If you're staring at an unexpected GitHub Actions bill, here's the order of operations:
Steps 1-3 are free and take an afternoon. Steps 4-5 require measurement over a week or two. Step 6 takes a few minutes to set up, then a billing cycle to compare. Step 7 is what keeps the savings permanent.
Tags
Recommended for you
What's next in your stack.