What Self-Hosted Runners Actually Cost
Self-hosted GitHub Actions runners bill idle instance hours plus image, autoscaling, upgrade and on-call work. A worked monthly model with the arithmetic.
Last verified:
A self-hosted GitHub Actions fleet bills three things your instance invoice shows partly or never: the instance hours you pay for while no job is running, the engineering time to build images, tune autoscaling and run upgrades, and the on-call attention when the fleet stops handing out runners. At 60,000 Linux job minutes per month, an example on-demand rate of $0.40 per hour and 12 hours of operating work at $120 per hour, the model below lands at $2,167.27 against $960.00 for the same minutes on warp-ubuntu-latest-x64-8x at $0.016 per minute.
Diagnosis
Teams compare a self-hosted fleet against per-minute pricing by putting one number on each side: the instance rate against the per-minute rate. That comparison leaves out four lines, and each of them scales differently from job minutes.
Idle instance hours. Instances bill wall-clock time. Job minutes bill job time. The ratio between them is fleet utilization, and it is the single number that decides whether the compute line is competitive. A pool sized for the 5 pm merge rush is mostly idle at 10 am, and a pool sized for the average queues at peak.
Runner image maintenance. GitHub sets the rebuild clock: "If you do not perform a software update within 30 days, the GitHub Actions service will not queue jobs to your runner" (runner software updates, checked on 2026-08-13). The actions/runner project shipped 11 stable releases in the 12 months to 2026-08-13, v2.328.0 through v2.336.0 (releases). Your toolchain versions ride on top of that cadence.
Autoscaling logic. On Actions Runner Controller, maxRunners and minRunners are static Helm values, and scheduled scaling is a cron job you write and maintain (deploy runner scale sets, checked on 2026-08-13). Whatever policy you build sits between the two failure modes above.
Upgrades and the support boundary. GitHub documents that Helm cannot upgrade ARC and that the procedure is an uninstall and reinstall, with a second cluster in another region as the documented way to avoid downtime (upgrading ARC). GitHub also recommends a Kubernetes expert on staff and publishes a list of Kubernetes topics outside GitHub Support's scope (ARC support scope, checked on 2026-08-13).
One more line hides behind caching. ARC ships no dependency cache of its own, and image caching sits on GitHub's out-of-scope list for ARC support, so a self-operated fleet usually grows a cache bucket, a registry mirror, or a persistent volume, each with its own storage bill and its own failure mode.
Where each line lands
| Cost line | What it bills | Which budget it hits |
|---|---|---|
| Instance hours while jobs run | Wall-clock time per instance | Cloud invoice |
| Instance hours while no job runs | Wall-clock time per instance | Cloud invoice |
| Cache bucket, registry mirror, artifact storage | Stored gigabytes and requests | Cloud invoice |
| Egress and address translation for private subnets | Per gigabyte processed | Cloud invoice |
| Image builds and toolchain updates | Engineer hours on a 30-day clock | Payroll |
| Autoscaling policy and capacity tuning | Engineer hours | Payroll |
| Controller and cluster upgrades | Engineer hours | Payroll |
| Failure triage and on-call | Engineer hours, at the worst time of day | Payroll |
Fix
Price the fleet with four numbers you already have, then compare the total against per-minute pricing at the same shape.
- Measure job minutes. Sum the wall-clock duration of completed jobs over a full month from the GitHub Actions API. This is the number a per-minute bill would charge for.
- Measure paid instance hours. Pull the same month from your cloud billing export, filtered to the tag or account that holds the runner fleet. Divide job hours by paid instance hours to get fleet utilization. Most fleets that never queue land well under 60 percent.
- Count operating hours. Log the work for one month against the four payroll lines above. Multiply by a loaded hourly rate.
- Compare against per-minute pricing at the same shape. Match vCPU and memory, then compare monthly totals rather than rates, because the two sides bill different units.
The three shapes worth comparing after that are a fleet you operate, hosted managed runners billed per job minute, and the middle path where the compute stays in your own cloud account. On the hosted side, and BYOC runs on AWS, GCP, and Azure with WarpBuild charging $0.002 per minute while your account pays for the instance (BYOC documentation). BYOC keeps the residency and network placement that drove self-hosting in the first place, and moves the image, autoscaling and upgrade lines onto WarpBuild.
Configuration
Measuring job minutes takes one command. Run it against a month of completed runs and sum the second column.
gh api --paginate \
"/repos/$OWNER/$REPO/actions/runs?status=completed&created=>=2026-07-01" \
--jq '.workflow_runs[].id' \
| while read -r run_id; do
gh api "/repos/$OWNER/$REPO/actions/runs/$run_id/jobs" \
--jq '.jobs[]
| select(.completed_at != null)
| [.runner_name,
(((.completed_at | fromdateiso8601) - (.started_at | fromdateiso8601)) / 60)]
| @tsv'
doneGroup the output by runner_name to separate the fleet you operate from anything else answering jobs. That column also tells you which runner labels carry the volume, which is where a rate change is worth making first.
Moving a job off the fleet is one line. The array label routes to a runner you registered, and a warp- label routes to a managed runner of the same shape:
name: ci
on:
push:
branches: [main]
jobs:
build-self-operated:
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v5
- run: ./scripts/build.sh
build-hosted:
runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v5
- run: ./scripts/build.sh
build-in-your-own-account:
runs-on: warp-custom-use1-linux-8x
steps:
- uses: actions/checkout@v5
- run: ./scripts/build.shwarp-ubuntu-latest-x64-8x is 8 vCPU and 32 GB from the cloud runners catalog. warp-custom-use1-linux-8x is a Runner ID from the custom runners page in the dashboard; the warp-custom- prefix is part of the ID. Running all three for a week on the same workflow gives you three real durations at the same shape, which is a better input to the model below than any published rate.
Cost or Time Model
Assumptions
| Input | Value | Source |
|---|---|---|
| Linux job minutes per month | 60,000 | Your run history, from the command above |
| Job shape | 8 vCPU, 32 GB | Your runs-on labels |
| Job hours implied | 1,000 | 60,000 divided by 60 |
| Fleet utilization, self-operated | 55 percent | Your billing export divided by job hours |
| Example on-demand instance rate | $0.40 per hour | Substitute your rate from AWS EC2 On-Demand pricing |
| Loaded engineering rate | $120 per hour | Your own figure |
| Operating hours per month, self-operated | 12 | The breakdown below |
| Operating hours per month, BYOC | 2 | Quota headroom, IAM review, region choice |
The 12 operating hours
| Work | Hours | What sets the clock |
|---|---|---|
| Image rebuilds and toolchain updates | 4 | The 30-day runner update rule, plus 11 actions/runner stable releases in the 12 months to 2026-08-13 |
| Autoscaling policy and capacity tuning | 2 | maxRunners and minRunners are static Helm values; scheduled scaling is a cron job you write |
| Controller and cluster upgrades | 3 | Helm cannot upgrade ARC; the documented path is uninstall and reinstall |
| Failure triage and on-call | 3 | Stuck pods, exhausted quotas, failed registrations |
Monthly totals
| Shape | Compute or fee | Engineering time | Monthly total |
|---|---|---|---|
| Fleet you operate, 55 percent utilization | 1,818.2 hours at $0.40 = $727.27 | 12 hours at $120 = $1,440.00 | $2,167.27, plus storage and network |
| GitHub-hosted 8-core Linux larger runner, list price | 60,000 at $0.022 = $1,320.00 | none | $1,320.00 |
warp-ubuntu-latest-x64-8x | 60,000 at $0.016 = $960.00 | none | $960.00 |
| BYOC at the example on-demand rate | $120.00 fee plus 1,000 hours at $0.40 = $520.00 | 2 hours at $120 = $240.00 | $760.00 |
| BYOC on spot at the AWS published ceiling | $120.00 fee plus 1,000 hours at $0.04 = $160.00 | 2 hours at $120 = $240.00 | $400.00 |
GitHub list prices were checked on 2026-08-13 on the GitHub Actions billing reference. WarpBuild rates come from the runner catalog on the pricing page.
BYOC runners cost $0.002 per minute in WarpBuild fees and the compute is billed by your own cloud account, where AWS states Spot Instances run at up to a 90 percent discount compared to On-Demand prices. AWS discount checked on 2026-08-13 on the AWS Spot page. That ceiling is an AWS figure rather than a WarpBuild measurement, and the rate you see depends on instance family, region, and the hour.
The two numbers that move the answer
Fleet utilization moves the compute line and nothing else:
| Utilization | Paid instance hours | Instance line at $0.40 per hour |
|---|---|---|
| 30 percent | 3,333.3 | $1,333.33 |
| 45 percent | 2,222.2 | $888.89 |
| 55 percent | 1,818.2 | $727.27 |
| 70 percent | 1,428.6 | $571.43 |
| 100 percent | 1,000.0 | $400.00 |
Operating hours move everything else. At 55 percent utilization the compute line is $727.27, which sits $232.73 below the hosted total of $960.00, so the fleet wins only while it costs under 1.94 hours per month at $120 per hour. At 100 percent utilization, a level no queue-free fleet reaches, the headroom is 4.67 hours per month. Both thresholds sit below the 12-hour estimate above, and neither counts the cache bucket, the registry mirror, or address translation charges on private subnets.
The model scales in one direction only. Compute lines and per-minute lines both grow with job minutes; the operating hours do not shrink when volume falls, which is why small fleets carry the worst ratio.
WarpBuild pricing is purely usage based. There is no base subscription fee, no platform fee, and no seat fee, and the whole rate card is on the pricing page. Signup includes $10 free credits, which is 625 minutes on warp-ubuntu-latest-x64-8x at $0.016 per minute: enough to run the three-label workflow above and get real durations before committing to any of these numbers.
FAQ
Is a self-hosted runner fleet cheaper than per-minute managed runners?
Only when the operating work is small. At 60,000 Linux job minutes per month on an 8 vCPU shape, 55 percent fleet utilization and a $0.40 per hour example instance rate, the compute line is $727.27 against $960.00 for warp-ubuntu-latest-x64-8x at $0.016 per minute. The gap is $232.73, so the fleet stops being the cheaper option once it takes more than 1.94 hours of engineering time per month at a loaded rate of $120 per hour.
What is the largest cost line that does not appear on the compute invoice?
The rebuild clock on runner images. GitHub states that if you do not perform a software update within 30 days, the GitHub Actions service will not queue jobs to your runner, and the actions/runner project shipped 11 stable releases in the 12 months to 2026-08-13, so image work recurs whether or not your toolchain changed.
Does running runners in my own cloud account remove the maintenance work?
It removes most of it. On BYOC the images, the autoscaling logic and the control plane upgrades belong to WarpBuild, and the instance stays in your account on your rates. What stays with you is the cloud account itself: quota headroom, IAM, and the region choice per stack. The hosted versus BYOC guide has the break-even instance rate for every runner size.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.