---
title: "Reduce GitHub Actions Costs Without Managing Servers"
description: "Reduce GitHub Actions spend by removing unnecessary work, fixing caches, right-sizing jobs, and comparing managed runners against your remaining free usage."
url: https://9apes.com/blog/cut-your-github-actions-bill/
---

# Reduce GitHub Actions Costs Without Managing Servers

## How can you reduce runner costs without managing servers?

Remove unnecessary runs, cancel superseded CI jobs, fix ineffective caches, and choose a runner size that matches the workload. Then compare managed runner services using the same workflow and the account's remaining included usage. A managed service can change compute costs without requiring your team to operate a runner fleet, but a switch only helps when the complete workload costs less.

Start with the changes below, then compare [current runner pricing](/pricing/). If you pilot 9apes, connect a repository and move one job using the [quick start](/docs/quick-start/). Keep the existing test coverage and build outputs unchanged so the comparison measures the runner and workflow changes you intended to make.

## 1. Stop running work that nothing depends on

The largest savings usually come from jobs that should not have run at all.

Path filters are the blunt instrument, and they work:

```yaml
on:
  pull_request:
    paths-ignore:
      - '**.md'
      - 'docs/**'
```

Use filters only when the skipped work does not provide required coverage.
A workflow skipped by path filtering can leave its required checks pending and
block merging. Check [GitHub's required-check guidance](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-required-status-checks)
and test documentation-only changes before adopting this example. Documentation
that contains executable examples may still need validation.

Concurrency groups are the other easy win. Without one, pushing three commits in
quick succession runs three full builds, and you only care about the last:

```yaml
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
```

Apply cancellation to replaceable validation work. Deployment, migration, and
release operations need their own completion and recovery policy. Measure the
minutes actually avoided in your repository rather than assuming a fixed saving.

## 2. Fix the cache before you touch anything else

A cache is useful when it avoids more work than restoring and saving it adds.
A successful job does not tell you whether the expensive work was reused. Measure
cache transfer, the remaining build, and saving together.

Check a recent run and inspect the first expensive step that misses. Compare its
inputs, cache keys, scope, branch, and previous export. A key that changes on every
run can prevent reuse, but dependency updates can produce legitimate misses.
The [Docker caching experiment](/blog/docker-layer-caching-github-actions/)
separates cache hits from useful time savings.

## 3. Right-size the runner

Compare the current rates for each suitable size. A larger runner saves money
only if its complete billed workload costs less. Extra CPU may not shorten a job
that spends most of its time waiting on a network call.

Run the same job on two sizes and compare duration, failures, and actual billing.
More cores help only where the job can use parallel work and has enough supporting
memory and I/O. Use the [build-time guide](/blog/reduce-github-actions-build-times/)
to identify the bottleneck before changing the default size.

## 4. Reconsider what the job runs on

Everything above is free and you should do it regardless. This one is a decision.

GitHub-hosted runners are priced per minute at published list rates that scale
with runner size. Third-party managed runners offer other infrastructure and operating models.
After connecting the provider's GitHub App, change one pilot job's runner label
and verify image compatibility, tool versions, network access, and outputs.

```diff
 jobs:
   test:
-    runs-on: ubuntu-latest
+    runs-on: 9apes
```

Two things move your bill when you switch, and it is worth separating them,
because providers routinely conflate them into one impressive percentage.

The first is the **rate**: what a minute costs. That is a straight comparison of
published prices at the same runner size.

The second is **duration**: how many minutes the job bills for. A faster machine
finishes sooner, so the meter runs for less time. This one is invisible on a
pricing page and it is often the larger effect.

They compound. If a runner is half the price and the job finishes in half the
time, the bill for that job falls by roughly 75%, not 50%. That is why per-job
savings quoted by runner vendors look so much larger than the difference in their
rate cards, and why you should ask which one a given number refers to before
putting it in a budget.

Use the [current rate card](/pricing/) and your own complete job durations for
cost estimates. The [published benchmarks](/benchmarks/) time narrower build
steps, with their own measurement boundaries; they do not establish invoiced
savings for your workflow. For standard GitHub-hosted runners, included usage and
free eligible public-repository usage can change whether a switch saves anything.
GitHub larger runners are always charged, including in public repositories, and
cannot use included minutes.

## 5. Look at where the minutes actually go

Most teams estimate this wrong. Before optimising anything, sort your workflows
by total minutes consumed and look at the top three. It is almost never the job
people complain about. That one is merely the most *annoying*, which is a
different metric from the most *expensive*.

A nightly job nobody watches, running a full matrix across six versions, will
quietly outspend the pull request check that everyone notices.

## The order that matters

Start with unnecessary work, replaceable concurrent runs, cache behavior, and
the resources your slow jobs actually use. Measure cost per successful workflow
after each change, including failures and retries.

Changing runners is worth doing after that, not instead of it. A wasteful
workflow on a cheaper runner is still a wasteful workflow.

If you want to size the runner question for your own repository rather than
trusting anyone's benchmark, the [pricing page](/pricing/) has a calculator that
takes your monthly minutes, and [the comparisons](/compare/) list published rates
for the main providers with the date each was checked.

For provider selection beyond the rate card, use the [managed runner guide](/blog/managed-github-actions-runners/) to review platform coverage, isolation, and operational responsibilities.
