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 theprandissuecommands, with theactions/setup-gocache. Mostly computation. - docker builds the image of Docker's sample app
(
docker/getting-started-app) onnode:22-slim, withnpm ci. Mostly downloads.
From one run to the other, only the label given to runs-on changes:
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
| Measure | GitHub | FFxF |
|---|---|---|
| Wait before the docker job | 2 s | 7 s |
| Wait before the go job | 2 s | 30 s |
| Building GitHub CLI | 97 s | 88 s |
| Tests | 24 s | 21 s |
| Whole go job | 152 s | 145 s |
docker build | 12 s | 39 s |
| Whole docker job | 14 s | 49 s |
| Minutes billed | 4 | 4 |
| Price of the run | 0.024 USD | 0.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
| Measure | GitHub | FFxF |
|---|---|---|
| Restoring the Go cache | 25 s | 23 s |
| Build | 3 s | 3 s |
| Tests | 6 s | 6 s |
| Whole go job | 46 s | 50 s |
docker build | 19 s | 43 s |
| Whole docker job | 24 s | 54 s |
| Minutes billed | 2 | 2 |
| Price of the run | 0.012 USD | 0.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:
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.
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-latestwithffxf-2vcpu-ubuntu-2404(or4vcpu,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.