GitHub Actions runners: half the price, measured

FFxF's GitHub Actions runners are open to everyone, in beta. The same workflow run at GitHub and at FFxF: a slightly faster build, half the bill, a slower cold docker build and 4 seconds with the Docker cache.

A workflow hands a job to a fresh machine, timed by a stopwatch, with its cache beside it; the machines of earlier jobs are already gone

FFxF's GitHub Actions runners are open to every account, in beta. On paper, the case fits in one line: 0.004 CAD a minute on 2 vCPU, which is 0.003 USD, against 0.006 USD for GitHub's Linux runner. What was left to check was what that means on a real workflow, in time as well as in money.

On October 22, 2026, we ran the same workflow file on ubuntu-latest at GitHub and on ffxf-2vcpu-ubuntu-2404 at FFxF, two 2-vCPU machines, twice in a row. Here are the numbers, including where we are slower.

1. The test workflow

Two independent jobs, picked because they look like ordinary CI:

  • go builds GitHub CLI (cli/cli, version 2.63.0) and runs the tests of the pr and issue commands, with the actions/setup-go cache. Mostly computation.
  • docker builds the image of Docker's sample app (docker/getting-started-app) on node:22-slim, with npm ci. Mostly downloads.

From one run to the other, only the label given to runs-on changes:

yaml
on:
  workflow_dispatch:
    inputs:
      label:
        default: ffxf-2vcpu-ubuntu-2404

jobs:
  go:
    runs-on: ${{ inputs.label }}
    steps:
      - uses: actions/checkout@v4
        with:
          repository: cli/cli
          ref: v2.63.0
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
      - run: go build -o bin/gh ./cmd/gh
      - run: go test ./pkg/cmd/pr/... ./pkg/cmd/issue/...

  docker:
    runs-on: ${{ inputs.label }}
    steps:
      - run: |
          git clone https://github.com/docker/getting-started-app.git app
          cd app
          # Dockerfile: FROM node:22-slim, npm ci --omit=dev, COPY . .
          docker build -t todo-app .

The machines, as the job itself reports them: an AMD EPYC (7763 on the first run, 9V74 on the second) at GitHub, an Intel Xeon Gold 6252 at FFxF, 2 vCPU on both sides. One detail matters for the comparison: ubuntu-latest now means Ubuntu 26.04 at GitHub, while our runners follow the ubuntu-24.04 image.

2. First run, no cache

MeasureGitHubFFxF
Wait before the docker job2 s7 s
Wait before the go job2 s30 s
Building GitHub CLI97 s88 s
Tests24 s21 s
Whole go job152 s145 s
docker build12 s39 s
Whole docker job14 s49 s
Minutes billed44
Price of the run0.024 USD0.016 CAD (0.012 USD)

On computation, our Xeon builds GitHub CLI 9 seconds faster and finishes the tests 3 seconds sooner. Both services round each job up to the minute: 4 minutes on each side, and a bill half the size at FFxF.

The wait before the go job comes from startup. We keep runners ready according to how many jobs the account starts at once; this repository had only ever started one at a time. The docker job took the ready runner, and the go job waited for a fresh machine to boot: 30 seconds. Those seconds are not billed, the minute only starts once the job has started.

The docker build, on the other hand, is three times slower at FFxF. The time goes into two downloads: the node:22-slim image (9.9 s against 2.7 s) and the packages of npm ci (9.8 s against 3.7 s). Our runners go out over IPv6; Docker Hub only has IPv4, and the containers of a build go out over IPv4 too, so these downloads go through address translation. At GitHub, they stay on a network next to the registries. This is our weak spot today, and the reason for the Docker cache, below.

The Set up runner step (6 to 8 s in our jobs) attaches the repository's Docker cache; it counts in the job's minutes.

3. Second run, warm caches

MeasureGitHubFFxF
Restoring the Go cache25 s23 s
Build3 s3 s
Tests6 s6 s
Whole go job46 s50 s
docker build19 s43 s
Whole docker job24 s54 s
Minutes billed22
Price of the run0.012 USD0.008 CAD (0.006 USD)

The setup-go cache (461 MB) restores just as fast on both sides. At FFxF it is kept in Montréal, next to the runners, with no change to the workflow, and billed 0.30 CAD per GB per month. With that cache, the build drops to 3 seconds everywhere.

On the Docker side, GitHub starts every job from an empty machine, so with no cached layer: 19 seconds. At FFxF, the Docker cache should have helped, and it did not. The rule at the time explains why: a repository's Docker cache was the one of its last successful job. Here the go job finished after the docker job, and its state is the one that was kept, although it never touched Docker. This is fixed since October 22.

4. The Docker cache, when it works

Since the fix, only a successful job that changed Docker updates the cache. The runner records the state of Docker at the start of the job (images, BuildKit cache, named volumes) and compares it at the end. The same two-job workflow, run a second time, gives this:

docker
FFxF: Docker cache of this repository attached (1.9 GB, saved 119 s ago), replaced only if this job changes Docker and succeeds.
#6 CACHED
#7 CACHED
#8 CACHED
docker build : 4 s (code 0)
FFxF: Docker changed by this job (images or build cache): if the job succeeded, its Docker state becomes the cache of this repository.
go
FFxF: Docker unchanged by this job: the Docker cache of this repository stays as it is.

Four seconds, against 39 to 43 seconds without the cache at FFxF and 12 to 19 seconds at GitHub. The whole job takes 14 seconds, 8 of them to attach the cache. The node:22-slim image is no longer downloaded, and npm ci only runs again when package-lock.json changes. The go job never touched Docker: it leaves the cache as it is, even when it finishes later.

To benefit from it:

  • turn the Docker cache on for the repositories that build images (console, GitHub runners, Docker cache card), with no change to your workflows;
  • the update happens at the end of the job and takes about ten seconds. A job that starts in the meantime, such as one behind a needs:, waits for it (30 seconds at most);
  • two jobs that change Docker at the same time start from the same cache, and the last one to succeed wins.

5. When it pays off

For the same minutes, the bill is cut in half on 2 and 4 vCPU, and a little more on 8 vCPU (0.012 USD a minute against 0.022 USD at GitHub). GitHub plans include a quota of minutes every month; jobs that run on our runners do not use it up, and the savings start as soon as your workflows go past it or need more than 2 vCPU.

Computation jobs (builds, tests) run at least as fast as at GitHub. Jobs that pull a lot from Docker Hub on every run are slower at FFxF as long as the Docker cache does not cover them.

6. Try it

  • In the console, open GitHub runners and click Connect GitHub: install the FFxF Runners app on an organization or on your account, and pick its repositories.
  • Replace ubuntu-latest with ffxf-2vcpu-ubuntu-2404 (or 4vcpu, 8vcpu) in a workflow.
  • Each repository's minutes show in the console and in the API. Every month, 500 minutes of 2 vCPU are free once the account has made a payment.

During the beta, only private GitHub.com repositories are served, on Linux x64. Sizes, prices and limits are on the GitHub runners page, and the technical details in the guide.

Three stacked layers: the disk has grown, the partition and the filesystem stretch after it

Growing a Linux disk live: growpart, ext4, XFS, LVM

After a move to a bigger plan, df often still shows the old size: the disk grew, the partition and the filesystem did not. ext4, XFS and LVM, each grown live on a test machine, with the real output.

Read the article 7 min read

Stacked disk images, one of them flagged with a warning sign

Cloud images for 28 systems: 8 traps we fixed

28 operating systems, each from its distribution's own cloud image, each with its own habits. The traps that cost us time, what you see when one of them bites, and the fix our images now carry.

Read the article 7 min read

Support & discussions

Technical questions, incident reports, or infrastructure discussions, the team is reachable on Discord, Telegram, X, Instagram, Reddit, and IRC.