Skip to content

ci: trial RunsOn runner sizes for Mutants and Test JS (do not merge) - #1297

Draft
auxesis wants to merge 6 commits into
mainfrom
ci/runs-on-trial
Draft

auxesis wants to merge 6 commits into
mainfrom
ci/runs-on-trial

Conversation

@auxesis

@auxesis auxesis commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Do not merge. This is a trial.

Summary

Our CI runs on Blacksmith runners, which are hosted machines that GitHub Actions jobs run on. We are evaluating RunsOn as a replacement. RunsOn starts a fresh EC2 machine for each job, in our own AWS account. Before we choose a runner, we need two things: proof that our builds pass on RunsOn, and a speed comparison across machine sizes.

This PR runs two jobs on several runners in the same workflow run: the current Blacksmith runner, GitHub's own runner, and three RunsOn sizes. All the runners build the same commit, so their times are directly comparable.

Changes

  • Mutants (mutants.yml, the mutation-testing gate for the stack-auth and stack-encrypt Rust crates): a matrix over blacksmith-16vcpu-ubuntu-2204 (current), ubuntu-22.04 (GitHub) and RunsOn c8a at 4, 8 and 16 vCPU.
  • Why c8a for Mutants: mutation testing is CPU-bound, and cargo-mutants tests one mutant at a time, so the speed of each core decides the run time. In RunsOn's EC2 benchmark, c8a (AMD Turin, 8th-generation compute-optimized) has the best x64 single-thread score (3,802). Its multi-core score (6,874) is about 1.7× that of Intel m8i (4,069), at about the same price (on-demand at the 2 vCPU size: $0.108/h vs $0.106/h). Older 7th-generation families cost about 5% less per hour, but their slower cores make the job run longer (rightsizing). Graviton (c8g) costs less, but it is arm64, which would add an architecture change to the comparison.
  • Test JS (tests.yml, job run-tests, the main TypeScript test suite with live encryption): a new runner axis over blacksmith-4vcpu-ubuntu-2404 (current), ubuntu-latest (GitHub) and RunsOn 2cpu, 4cpu and 8cpu. It runs for both Node 22 and Node 24, so there are 10 jobs.
  • RunsOn labels:
    • volume=80gb: the default disk fills during a Rust build, and the linker then crashes (found in ci(test): trial RunsOn runner sizes (do not merge) vitaminc#487).
    • extras=s3-cache: the job cache is served from the RunsOn stack's own S3 bucket. Fetching GitHub's cache from AWS cost 45–100 s per job in that trial.
    • Mutants only: image=ubuntu22-full-x64, to match the Ubuntu 22.04 that its Blacksmith baseline uses. RunsOn defaults to 24.04.
  • Mutants does fixed work. This PR changes no crate source, so its own diff would give zero mutants, and every runner would finish in seconds. The trial mutates the lines of a fixed commit range instead (cef0a1c..9b7e122, the 2026-10-07/08 error-message work, 15 mutants).
  • Mutants: mise's tool cache is off. A guard test (workflow-mise-cache-image.test.mjs) requires this when runs-on comes from a matrix. So every entry installs cargo-nextest and cargo-mutants from scratch. To compare the runners themselves, use the time of the "Run cargo-mutants" step, not the job total.
  • Mutants: only the Blacksmith entry posts the sticky PR comment, so the five entries do not race to write it.
  • Both matrices: fail-fast: false, so a failure on one runner does not cancel the others.

Verification

  • pnpm run test:scripts: 79 files passed, 1,387 tests passed (29 skipped). This includes the guard tests that read these two workflows: the mise cache key, the pinned Node and Postgres matrix, the binding step order and node-gyp.
  • e2e/tests/supply-chain.e2e.test.ts: 29 tests passed.
  • CI on this PR: results will be posted as comments.

Related

Review notes

  • The first run is cold on GitHub and RunsOn, because each has its own cache. Blacksmith's cache is warm from main. A re-run gives warm timings for all runners.
  • RunsOn uses spot instances by default and retries a job if AWS interrupts its instance.
  • Mutants pins only c8a, so that each size runs on the same CPU and the timings compare cleanly. If spot capacity for c8a is short, a later change can add a fallback family (for example family=c8a+c8i), at the cost of noisier timings.
  • cargo-mutants tests one mutant at a time (.cargo/mutants.toml sets no jobs). So the larger sizes may help less than their price grows. Setting --jobs is a separate experiment.
  • Test JS keeps RunsOn's built-in Ncpu-linux-x64 runners for now. The c8a choice was made for the CPU-heavy Mutants job, and the same change can be tried on Test JS later.
  • After a size is chosen, a separate PR collapses each matrix back to one runner. That PR also has to teach workflow-mise-cache-image.test.mjs to read the image from a RunsOn label, so the mise cache can come back on.

🤖 Generated with Claude Code

https://claude.ai/code/session_017GMWYNGir7bpQacaUzN84U

Runs the Mutants gate and Test JS's run-tests job on the current
Blacksmith runner, on GitHub's runner and on several RunsOn sizes, in
one workflow run, so the runners can be compared on the same commit
before choosing one. Collapse each matrix back to one runner before
merging.

The RunsOn labels carry the two fixes cipherstash/vitaminc#487 found:
an 80 GB volume, because the default root volume fills during a Rust
build and rust-lld dies with SIGBUS, and the s3-cache extra, which
serves actions/cache from the RunsOn stack's own bucket instead of
fetching GitHub's cache over the internet. The Mutants entries also
ask for the Ubuntu 22.04 image to match the Blacksmith baseline.

The Mutants entries pin the c8a family. cargo-mutants builds and
tests one mutant at a time, so per-core speed decides its run time,
and c8a has the best x64 single-thread score in RunsOn's EC2
benchmark at about the price of m8i.

The Mutants trial mutates a fixed stack-auth/stack-encrypt commit
range (15 mutants) instead of the PR diff, which touches no crate
source and would yield no mutants. Its mise cache is off, because a
matrix runs-on names no image to key the cache by, which
workflow-mise-cache-image.test.mjs requires.

Claude-Session: https://claude.ai/code/session_017GMWYNGir7bpQacaUzN84U
@changeset-bot

changeset-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: dd4df4d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Mutation testing (cargo-mutants, --in-diff, stack-auth + stack-encrypt)

caught missed unviable timeout
68 0 57 0

Every mutant in the changed lines was caught by a test.

@auxesis

auxesis commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Trial results: every runner passes

All 15 trial jobs passed on the first attempt: Mutants 5 of 5, Test JS 10 of 10. They passed again on a re-run (attempt 2). The re-run gives warm-cache timings for every runner. On attempt 1 only Blacksmith had a warm cache, restored from main.

Times are in minutes. Attempt 2 is the warm run.

Mutants (mutation testing, 15 mutants)

Runner Instance Job (cold) Job (warm) mise install cargo-mutants
RunsOn c8a 16cpu c8a.4xlarge (spot) 3.3 3.2 1.1 1.9
Blacksmith 16vcpu (current) 3.9 3.7 1.3 2.1
RunsOn c8a 8cpu c8a.2xlarge (spot) 4.1 4.2 1.6 2.4
RunsOn c8a 4cpu c8a.xlarge (spot) 6.0 5.9 2.4 3.3
GitHub ubuntu-22.04 12.7 12.0 5.0 6.7
  • c8a 16cpu is the fastest: about 10% faster than the current Blacksmith 16vcpu in the mutation step, and about 14% faster over the whole job.
  • c8a 8cpu is close: its mutation step is about 15% slower than Blacksmith 16vcpu, with half the vCPUs.
  • Cold and warm times are almost the same, so the Rust cache matters little for this job.
  • A third or more of each job is the mise tool install. It runs because the trial turns mise's cache off. With the cache on, every runner would save most of that time.
  • This workload is light: about 2 minutes of mutation testing. The real gate averaged 11.9 minutes per run last week, so on a larger PR the gaps between sizes will be larger.

Test JS (run-tests, live encryption suite)

Runner Instance Node 22 cold / warm Node 24 cold / warm
Blacksmith 4vcpu (current) 3.8 / 3.8 3.7 / 3.4
RunsOn 8cpu m8id.2xlarge, c8i-flex.2xlarge (spot) 5.0 / 3.7 4.8 / 3.7
GitHub ubuntu-latest 7.7 / 4.3 9.2 / 4.8
RunsOn 4cpu m8id.xlarge, m8i-flex.xlarge (spot) 6.7 / 4.8 6.5 / 4.6
RunsOn 2cpu m8id.large, m8i-flex.large, m8i.large (spot) 11.7 / 7.1 11.2 / 6.8
  • Warm, RunsOn 8cpu matches Blacksmith 4vcpu, and RunsOn 4cpu roughly matches GitHub.
  • The cold run is slow because of the Rust binding builds. The protect-ffi binding took 1.2–4.0 minutes to build cold, and 0.0–0.1 minutes once cached.
  • RunsOn's built-in Ncpu-linux-x64 runners chose a different instance family on each run (m8id, m8i-flex, c8i-flex, m8i). That makes Test JS timings noisier than the Mutants timings, which are pinned to c8a.

Not yet measured

  • Cost per job. RunsOn's benchmark gives spot prices for the 2 vCPU size in us-east-1. Price per job needs this stack's actual region, spot price and RunsOn licence.
  • Test JS on a pinned family. For example family=c8a/cpu=4 or cpu=8. It would give cleaner Test JS numbers.
  • Mutants with mise's cache on. It would show the job times a merged setup would actually get.

The first trial left two questions open. RunsOn's built-in
Ncpu-linux-x64 runners chose a different instance family on every
Test JS run (m8id, m8i-flex, c8i-flex, m8i), so the size comparison
mixed CPU generations. Pinning family=c8a, as the Mutants entries
already do, holds the CPU constant across sizes.

The Mutants workload was 15 mutants, about two minutes of mutation
testing, while the real gate averaged 11.9 minutes a run last week.
The fb75f40..9b7e122 range of stack-auth/stack-encrypt changes
yields 125 mutants, so differences between sizes show up at the
scale the gate actually runs.

Claude-Session: https://claude.ai/code/session_017GMWYNGir7bpQacaUzN84U
Runs each job of Test EQL, Tests (Go), Tests (crates), Test JS, the
four integration suites, Fuzz (crates), Unused dependencies (crates)
and CRAP gate (crates) on its current Blacksmith runner, on GitHub's
runner, and on three RunsOn c8a sizes, so every job gets the same
side-by-side comparison the Mutants and run-tests trials gave.

Each job's sizes follow its current tier: a 16 vCPU Ubuntu 22.04 job
runs on c8a 4, 8 and 16 cpu with the ubuntu22 image, and a 4 vCPU
Ubuntu 24.04 job on c8a 2, 4 and 8 cpu with the ubuntu24 image.
GitHub's entry names the same Ubuntu release, so every entry of a job
runs one image.

That is what lets the mise caches stay on. workflow-mise-cache-image
now resolves a matrix runs-on to the labels of its axis and accepts
the prefix when every label names the same image, reading a RunsOn
label's image= segment. A RunsOn label without one names no image,
because its default is stack configuration the label does not show.

Fuzz's include-only matrix becomes an explicit target-by-runner cross
product: a runner axis beside include rows would merge every row into
every runner, and each later row would overwrite the one before.

Two jobs upload an artifact other jobs download (Test EQL's
nextest-archive, Go's wasm-guests). An artifact name is unique per
run, so only the Blacksmith entry uploads, and every runner's
consumers test that build.

Left on their current runners: Test EQL's changes, setup and
ci-required, which publish outputs or aggregate results; Go's macOS
and Windows jobs, which RunsOn does not cover; and the schedule-only
fuzz campaign, which a pull request never runs.

Claude-Session: https://claude.ai/code/session_017GMWYNGir7bpQacaUzN84U
@auxesis

auxesis commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Experiments 1 and 2: pinned c8a for Test JS, 125 mutants for Mutants

Both experiments passed on every runner (commit 0f2123b). Times are in minutes. Every runner had a warm cache, except where a note says otherwise.

Experiment 1: Test JS run-tests pinned to family=c8a

Runner Instance Node 22 Node 24
RunsOn c8a 8cpu c8a.2xlarge (spot) 3.1 2.9
RunsOn c8a 4cpu c8a.xlarge (spot) 3.4 3.3
Blacksmith 4vcpu (current) 3.7 4.0
RunsOn c8a 2cpu c8a.large (spot) 5.1 4.9
GitHub ubuntu-latest 6.5 6.3
  • c8a 4cpu is about 12% faster than Blacksmith 4vcpu, with the same number of vCPUs. c8a 8cpu is about 22% faster.
  • Pinning the family helped. On the warm run of the built-in runners, 4cpu (m8i-flex) took 4.6–4.8 minutes, and 2cpu (m8i) took 6.8–7.1. On c8a they took 3.3–3.4 and 4.9–5.1.
  • The RunsOn S3 cache carried over from the earlier runs, so these RunsOn entries were warm on their first attempt.

Experiment 2: Mutants with 125 mutants

The trial now mutates fb75f40..9b7e122. That is 125 mutants, so the Blacksmith job now takes 12.9 minutes. That is close to the real gate's average of 11.9 minutes per run last week.

Runner Instance Job cargo-mutants step
RunsOn c8a 16cpu c8a.4xlarge (spot) 10.6 9.2
Blacksmith 16vcpu (current) 12.9 11.4
RunsOn c8a 8cpu c8a.2xlarge (spot) 14.0 12.2
RunsOn c8a 4cpu c8a.xlarge (spot) 20.5 17.9
GitHub ubuntu-22.04 28.8 25.0
  • c8a 16cpu is about 18% faster than Blacksmith 16vcpu over the whole job, and 19% faster in the mutation step.
  • c8a 8cpu is about 7% slower than Blacksmith 16vcpu in the mutation step, with half the vCPUs.
  • Going from 8 to 16 cpu saves about 25% of the time while doubling the vCPUs. cargo-mutants tests one mutant at a time, so more cores help less than linearly.
  • mise's tool install is still uncached in this job (1.1–3.6 minutes), as noted in the PR description.

Next

The next commit extends the same comparison to every job in Test EQL, Tests (Go), Tests (crates), Test JS, the four integration suites, Fuzz (crates), Unused dependencies (crates) and CRAP gate (crates).

The GitHub entry of Shard PG17 2/4 failed nine payload_schema_tests
with "failed to read /home/runner/_work/stack/stack/packages/eql/...".
Those tests locate their schema files through
env!("CARGO_MANIFEST_DIR"), which the compiler bakes into the binary,
and the shards ran the archive Blacksmith built. Blacksmith and
RunsOn check out under _work/, GitHub-hosted runners under work/, so
only GitHub's shards looked in a directory that was not there.

Each build-archive entry now uploads nextest-archive-<runner id> and
each shard downloads its own runner's, which also times the whole
build-then-test pipeline on every runner.

Claude-Session: https://claude.ai/code/session_017GMWYNGir7bpQacaUzN84U
@auxesis

auxesis commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Phase 2 results: every job in 12 workflows, on 5 runners each

Commit 8f19afd ran 44 jobs on 5 runners each, 220 trial jobs in total. The jobs come from Test EQL, Tests (Go), Tests (crates), Test JS, the four integration suites, Fuzz (crates), Unused dependencies (crates), CRAP gate (crates) and Mutants. The runners for each job are its current Blacksmith runner, GitHub's own runner, and RunsOn c8a at three sizes.

216 of 220 jobs passed. The 4 failures are Test EQL's shards on GitHub's runner, and they come from the trial setup, not from the runner. The shards replayed a test archive that Blacksmith built. That archive has Blacksmith's checkout path compiled into it (env!("CARGO_MANIFEST_DIR")), and GitHub checks out to a different path, so the tests could not find their schema files. Commit 4058c00 gives each runner its own archive. It has not been pushed yet; this comment comes first.

Summary

  • Same speed at the same size. On c8a with the vCPU count a job has today, all 44 jobs together took 0.98× Blacksmith's time: 0.92× for the 16-vCPU jobs and 1.03× for the 4-vCPU jobs.
  • About a third of the cost at the same size. Projected over last week's volume for these 44 jobs, c8a at the same size costs about $400 a week. Blacksmith costs about $1,240.
  • RunsOn's numbers here are pessimistic. This was the first run of most of these jobs on GitHub and RunsOn, so their caches were cold. Blacksmith's caches were warm from main. Only Test JS run-tests and Mutants had run before.
  • GitHub's runner is free on a public repository, but slow. It took 1.8–2.4× Blacksmith's time.
  • RunsOn jobs start about 40 seconds later. Each job boots its own EC2 instance. The median wait from queued to started was 37–49 s on RunsOn, 6–7 s on Blacksmith and 2 s on GitHub.
  • 25% of RunsOn jobs fell back to on-demand instances. 33 of 132 jobs could not get a spot instance. On-demand cost 2.1–2.3× spot per minute in this run, and the costs below include that mix. Pinning one family (c8a) narrows the spot pool. Allowing a second family (for example family=c8a+c8i) should raise the spot share, at the cost of more varied CPU speed.
  • Memory was never short. c8a has 2 GB per vCPU, half of Blacksmith's 4 GB. RunsOn reported "no significant resource constraints" for 131 of the 132 jobs; the last one printed no report.

Options, projected over last week's volume

The projection takes each job's cost in this run and multiplies it by the number of times that job ran last week. These 44 jobs ran 16,017 times last week.

Option 16-vCPU jobs on 4-vCPU jobs on Time vs today Weekly cost vs today
Today Blacksmith 16vcpu Blacksmith 4vcpu 1.00× $1,237 –
RunsOn, same vCPU c8a 16cpu c8a 4cpu 0.98× $402 −67%
RunsOn, faster c8a 16cpu c8a 8cpu 0.88× $459 −63%
RunsOn, 8 cpu everywhere c8a 8cpu c8a 8cpu 0.90× $318 −74%
RunsOn, cheapest at today's speed c8a 8cpu c8a 4cpu 1.00× $261 −79%
GitHub-hosted ubuntu-22.04 ubuntu-24.04 2.09× $0 −100%
  • RunsOn fixed costs come on top. These jobs run about 69,000 times a month, which puts RunsOn on its Growth licence: €900 a year, about €17 a week. Its AWS control plane costs from about $7 a month.
  • Blacksmith's actual cost last week was about $1,115, computed as last week's minutes × list price. The $1,237 above uses this run's job times instead, so that every option is projected the same way.

Price per minute

Runner vCPU / RAM Spot On-demand
Blacksmith 4vcpu 4 / 16 GB $0.0080
Blacksmith 16vcpu 16 / 64 GB $0.0320
c8a.large (RunsOn 2cpu) 2 / 4 GB $0.0012 (no sample)
c8a.xlarge (RunsOn 4cpu) 4 / 8 GB $0.0019 $0.0040
c8a.2xlarge (RunsOn 8cpu) 8 / 16 GB $0.0036 $0.0076
c8a.4xlarge (RunsOn 16cpu) 16 / 32 GB $0.0065 $0.0148
GitHub ubuntu-22.04 / ubuntu-24.04 4 / 16 GB free on a public repository
  • RunsOn prices are what this run actually cost in us-west-2. They come from RunsOn's own per-job estimate: EC2 plus the 80 GB gp3 root volume, billed per second, including boot time and 20 s of shutdown.
  • Blacksmith prices are its list price. Blacksmith charges $0.004 per minute for 2 vCPU, and its docs say larger runners use minutes in proportion to their vCPU count. Its billing granularity is not published.

By tier

16 vCPU tier (Ubuntu 22.04): 22 jobs

Runner Minutes, all jobs once vs Blacksmith (total) Median job vs Blacksmith Cost, all jobs once Projected weekly cost Projected weekly minutes
blacksmith 16vcpu 92 1.00× 1.00× $2.93 $975 30,479
github 215 2.35× 2.36× $0.00 $0 71,978
c8a 4cpu 110 1.20× 1.25× $0.31 $107 36,915
c8a 8cpu 89 0.97× 1.09× $0.51 $176 30,090
c8a 16cpu 85 0.92× 1.00× $0.91 $317 28,466

Last week these 22 jobs ran 7,940 times for 26,292 minutes on blacksmith 16vcpu.

4 vCPU tier (Ubuntu 24.04): 22 jobs

Runner Minutes, all jobs once vs Blacksmith (total) Median job vs Blacksmith Cost, all jobs once Projected weekly cost Projected weekly minutes
blacksmith 4vcpu 96 1.00× 1.00× $0.77 $262 32,720
github 176 1.84× 1.81× $0.00 $0 60,340
c8a 2cpu 145 1.52× 1.47× $0.18 $64 49,874
c8a 4cpu 99 1.03× 0.99× $0.25 $85 33,730
c8a 8cpu 80 0.83× 0.80× $0.42 $142 27,257

Last week these 22 jobs ran 8,077 times for 34,250 minutes on blacksmith 4vcpu.

Queue to start, and spot against on-demand

Runner Jobs Median queued (s) p90 queued (s) Spot On-demand
blacksmith 16vcpu 22 7 8 – –
blacksmith 4vcpu 22 6 6 – –
c8a 16cpu 22 49 59 13 9
c8a 2cpu 22 46 57 22 0
c8a 4cpu 44 47 58 34 10
c8a 8cpu 44 37 65 30 14
github 44 2 2 – –

Where RunsOn was slower, and why

  • Integration suites on c8a 4cpu were slower than Blacksmith 4vcpu. Drizzle took 6.3 against 4.2 minutes, and protect-ffi 6.3 against 4.5. In Drizzle (db=supabase), the test steps themselves were slightly faster on c8a: 40 s + 46 s against 43 s + 49 s. The gap is setup:

    • Build the protect-ffi binding took 102–110 s on RunsOn, because its cache was cold. On Blacksmith it was a cache hit.
    • Start supabase took 26 s on RunsOn, against under 10 s on Blacksmith. It pulls Docker images onto a fresh instance.

    A warm re-run removes the first. The second recurs on every fresh instance, unless the images are cached, for example with RunsOn's ecr-pull-through extra. That is not tested here.

  • Test EQL shards ran faster on fewer cores. On c8a 4cpu and 8cpu they took 3.7–4.1 minutes. On c8a 16cpu they took 4.7–5.2, and on Blacksmith 16vcpu 6.2–7.2. More vCPUs made them slower. Contention on the shard's Postgres is a likely cause, but I have not investigated it.

  • Fuzz regression: c8a 4cpu matches Blacksmith 4vcpu (7.2–7.6 against 7.6–8.7 minutes), and c8a 8cpu is about 30% faster (5.4–5.6).

  • Mutants: c8a 16cpu took 10.4 minutes against 11.7 on Blacksmith 16vcpu. That matches experiment 2.

Method

  • One run, at 8f19afd.
  • Minutes are each job's start to finish, so they exclude queue time.
  • RunsOn cost is the estimate RunsOn prints at the end of each job.
  • Blacksmith cost is minutes × list price.
  • GitHub's standard runners are free on a public repository.
  • The weekly projection is last week's run count for each job, times that job's cost in this run.
  • GitHub's totals include the four failed shards, timed up to the point where they failed.
  • The two tiers are each job's current Blacksmith size. A 16 vCPU Ubuntu 22.04 job ran on c8a 4, 8 and 16 cpu with the ubuntu22 image. A 4 vCPU Ubuntu 24.04 job ran on c8a 2, 4 and 8 cpu with the ubuntu24 image.
  • Not in the trial: Test EQL's changes, setup and ci-required stayed on Blacksmith, because they publish outputs or collect results. Go's macOS and Windows jobs stayed on GitHub, because RunsOn does not cover them. The schedule-only fuzz campaign never runs on a pull request.

Next

Pushing 4058c00 runs all twelve workflows again. Their caches are now warm on GitHub and RunsOn too, so that run gives the like-for-like comparison. I'll post its numbers here.

Per job (minutes; RunsOn cost in brackets)

16 vCPU tier (Ubuntu 22.04)
Workflow Job blacksmith 16vcpu github c8a 4cpu c8a 8cpu c8a 16cpu
CRAP gate (crates) CRAP (stack-auth) 4.4 14.0 6.6 ($0.014) 4.8 ($0.019) 4.4 ($0.032)
CRAP gate (crates) CRAP (stack-encrypt) 4.4 14.2 6.9 ($0.014) 4.8 ($0.020) 4.2 ($0.031)
Mutants Mutants gate 11.7 38.2 20.6 ($0.042) 13.9 ($0.053) 10.4 ($0.071)
Test EQL Build test archive 7.8 18.5 9.8 ($0.041) 9.0 ($0.072) 9.1 ($0.143)
Test EQL Encrypted-domain codegen 0.6 0.8 0.6 ($0.004) 0.6 ($0.009) 0.6 ($0.016)
Test EQL JSON Schema validation 0.6 2.6 1.3 ($0.007) 0.9 ($0.011) 0.8 ($0.020)
Test EQL Matrix coverage inventory 4.2 9.8 5.7 ($0.025) 4.5 ($0.038) 4.2 ($0.070)
Test EQL Rust workspace crates 1.5 5.2 3.4 ($0.016) 2.5 ($0.024) 2.3 ($0.042)
Test EQL SQL doc validation 0.7 1.1 0.6 ($0.002) 0.6 ($0.009) 0.7 ($0.008)
Test EQL Shard PG17 1/4 6.8 12.4 failure 4.1 ($0.009) 4.0 ($0.015) 5.2 ($0.037)
Test EQL Shard PG17 2/4 7.2 5.6 failure 4.0 ($0.008) 4.0 ($0.015) 5.1 ($0.037)
Test EQL Shard PG17 3/4 7.2 7.9 failure 3.9 ($0.008) 4.0 ($0.015) 4.7 ($0.034)
Test EQL Shard PG17 4/4 6.2 10.5 failure 3.7 ($0.008) 3.9 ($0.014) 4.7 ($0.034)
Test EQL Supabase splinter 0.8 1.4 0.8 ($0.005) 0.8 ($0.010) 0.9 ($0.021)
Test EQL Validate (Postgres 17) 0.8 1.6 0.9 ($0.003) 0.9 ($0.011) 1.0 ($0.023)
Test EQL doc anchor links 0.1 0.6 0.4 ($0.002) 0.2 ($0.004) 0.2 ($0.006)
Test EQL e2e property suite (fresh encryption) 9.4 19.3 10.4 ($0.044) 9.8 ($0.078) 9.3 ($0.146)
Test EQL eql_v3 self-containment 0.6 1.2 0.7 ($0.005) 0.6 ($0.009) 0.6 ($0.018)
Test EQL known-failure markers 0.2 0.6 0.4 ($0.002) 0.4 ($0.004) 0.2 ($0.006)
Tests (Go) WASI check, guests and Go (Linux) 8.4 23.9 14.3 ($0.028) 9.8 ($0.038) 8.2 ($0.056)
Tests (crates) fmt, clippy, nextest, doctests, rustdoc 6.5 20.9 8.1 ($0.017) 7.3 ($0.029) 5.9 ($0.042)
Tests (crates) node bindings, napi typings, stack-auth-wasm 1.8 5.3 2.5 ($0.006) 2.0 ($0.009) 1.9 ($0.016)
4 vCPU tier (Ubuntu 24.04)
Workflow Job blacksmith 4vcpu github c8a 2cpu c8a 4cpu c8a 8cpu
Fuzz (crates) fuzz regression (access-key) 8.0 11.3 11.9 ($0.014) 7.6 ($0.016) 5.5 ($0.046)
Fuzz (crates) fuzz regression (check-record) 7.9 13.5 11.6 ($0.014) 7.5 ($0.032) 5.4 ($0.022)
Fuzz (crates) fuzz regression (client-key) 7.7 14.4 11.3 ($0.014) 7.2 ($0.014) 5.4 ($0.022)
Fuzz (crates) fuzz regression (jwt-decode) 8.2 15.0 12.0 ($0.015) 7.6 ($0.016) 5.6 ($0.023)
Fuzz (crates) fuzz regression (plan-build) 7.6 13.7 11.5 ($0.015) 7.2 ($0.014) 5.5 ($0.046)
Fuzz (crates) fuzz regression (sealed-value) 8.0 9.5 11.4 ($0.014) 7.2 ($0.031) 5.5 ($0.022)
Fuzz (crates) fuzz regression (term-decode) 8.7 14.5 11.4 ($0.015) 7.2 ($0.015) 5.5 ($0.022)
Integration — Drizzle (EQL v3) Drizzle v3 integration (db=postgres) 4.2 7.8 7.5 ($0.009) 6.3 ($0.013) 5.6 ($0.022)
Integration — Drizzle (EQL v3) Drizzle v3 integration (db=supabase) 4.2 12.2 7.9 ($0.010) 6.3 ($0.013) 5.8 ($0.048)
Integration — Supabase (EQL v3) Supabase v3 integration (db=supabase) 2.3 3.5 3.5 ($0.005) 2.6 ($0.006) 2.5 ($0.011)
Integration — prisma-next (EQL v3) prisma-next v3 integration (db=postgres) 2.0 3.9 2.8 ($0.004) 1.9 ($0.005) 1.8 ($0.009)
Integration — prisma-next (EQL v3) prisma-next v3 integration (db=supabase) 2.1 3.9 3.1 ($0.004) 2.4 ($0.006) 2.0 ($0.019)
Integration — protect-ffi (native + WASM) protect-ffi integration suite 4.5 13.9 8.5 ($0.010) 6.3 ($0.014) 5.6 ($0.023)
Test JS Lint (Biome) 0.3 0.3 0.4 ($0.001) 0.3 ($0.002) 0.3 ($0.004)
Test JS Run E2E Tests 1.2 2.2 1.9 ($0.003) 1.2 ($0.004) 0.9 ($0.005)
Test JS Run Tests (Bun) 1.5 3.9 2.5 ($0.004) 1.5 ($0.004) 1.1 ($0.006)
Test JS Run Tests (Node 22) 3.5 6.5 5.2 ($0.007) 3.3 ($0.008) 2.9 ($0.012)
Test JS Run Tests (Node 24) 3.5 5.9 4.8 ($0.006) 3.2 ($0.008) 2.7 ($0.012)
Test JS Run WASM E2E Tests (Deno) 2.8 7.9 6.5 ($0.008) 4.6 ($0.010) 4.0 ($0.017)
Tests (Go) Go lint 0.7 1.0 0.8 ($0.002) 0.7 ($0.003) 0.6 ($0.004)
Tests (Go) Go live tests and stack-encrypt examples 2.2 3.5 3.0 ($0.004) 2.2 ($0.005) 1.7 ($0.007)
Unused dependencies (crates) Check unused Rust dependencies 4.8 7.8 6.0 ($0.008) 4.4 ($0.009) 4.0 ($0.017)

@auxesis

auxesis commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Warm run results: all 220 trial jobs pass

Commit 4058c00 gives each Test EQL runner its own test archive. With it, every job passed on every runner, including the four Test EQL shards on GitHub's runner that failed on the previous run. All 44 jobs had run once before on every runner, so this run is the like-for-like comparison with warm caches.

A correction to the previous comment. It said Blacksmith's caches were warm from main. For 14 of the Rust jobs they were not: CRAP, Fuzz regression, udeps, Tests (crates) and the two Linux Go jobs. On Blacksmith those jobs took 1.6–9× longer on the first run than on this one. For example, fuzz regression took 7.6–8.7 minutes on the first run and 1.2–1.6 minutes on this one. The first-run fuzz log on Blacksmith says mise cache not found and No cache found. So on the first run those jobs were cold on every runner. I have not found out why Blacksmith missed. Three of the workflows (udeps, Tests (crates) and Tests (Go)) also run on push, so "they only run on pull requests" does not explain it.

Summary

  • c8a at the same vCPU count matches Blacksmith's speed: 0.99× its total time across all 44 jobs. That is 0.98× for the 16-vCPU jobs and 1.02× for the 4-vCPU jobs.
  • It costs about 60% less: a projected $326 a week, against $843 on Blacksmith, at last week's volume for these jobs.
  • c8a 8cpu everywhere is 7% faster and 77% cheaper than Blacksmith: $194 a week. It is a step up for the 4-vCPU jobs and a step down for the 16-vCPU ones, and it was faster on both.
  • The integration suites' gap closed. On the first run they were slower on c8a 4cpu. With a warm cache, Drizzle took 4.2–4.3 minutes on c8a 4cpu, against 4.1–4.5 on Blacksmith 4vcpu, and protect-ffi took 4.1 on both. The cold Build the protect-ffi binding step had caused the gap.
  • Test EQL shards are faster on smaller c8a instances: 3.7–4.1 minutes on c8a 4cpu and 8cpu, against 5.5–6.9 on Blacksmith 16vcpu and 4.7–5.2 on c8a 16cpu. I see the same pattern on both runs, and I haven't found the cause.
  • Unchanged from the first run:
    • RunsOn jobs start 40–50 s later than Blacksmith's, because each one boots an instance.
    • 25% of RunsOn jobs ran on on-demand instances, at about 2.2× the spot price.
    • RunsOn reported no resource constraints.
  • GitHub's runner took 2.46× Blacksmith's time, and up to 4.8× on the integration suites (protect-ffi took 19.6 minutes, against 4.1).

Options, projected over last week's volume (warm run)

Option 16-vCPU jobs on 4-vCPU jobs on Time vs today Weekly cost vs today
Today Blacksmith 16vcpu Blacksmith 4vcpu 1.00× $843 –
RunsOn, same vCPU c8a 16cpu c8a 4cpu 0.99× $326 −61%
RunsOn, faster c8a 16cpu c8a 8cpu 0.94× $349 −59%
RunsOn, 8 cpu everywhere c8a 8cpu c8a 8cpu 0.93× $194 −77%
RunsOn, cheapest at today's speed c8a 8cpu c8a 4cpu 0.99× $171 −80%
GitHub-hosted ubuntu-22.04 ubuntu-24.04 2.46× $0 −100%
  • RunsOn fixed costs come on top: the Growth licence at €900 a year (about €17 a week), plus its AWS control plane from about $7 a month.
  • The projections from the first run were higher (Blacksmith $1,237, same-vCPU c8a $402), because so many caches were cold. A new pull request sees something like the first run's numbers, because its caches start cold. Every later push to it sees numbers like these.
  • The method is the same as before: last week's run count for each job, times that job's cost in this run. RunsOn costs are its own per-job estimate, at the spot and on-demand mix it actually got. Blacksmith costs are its list price of $0.004 per 2 vCPU-minute, scaled by vCPU count.

Recommendation

c8a 8cpu for both tiers. It is the cheapest option that is faster than today on both tiers:

  • 16-vCPU jobs: 0.97× Blacksmith's time.
  • 4-vCPU jobs: 0.87× Blacksmith's time.
  • Cost: about a quarter of today's.

One job is worth keeping at 16 cpu: Mutants. It took 13.9 minutes on c8a 8cpu, against 10.7 on c8a 16cpu and 11.6 on Blacksmith. On c8a 16cpu it would cost about $0.07 a run.

Before moving, two things are worth checking:

  • Spot capacity. A wider family list, for example family=c8a+c8i, should reduce the on-demand fallback.
  • The 40–50 s start delay on short jobs.

By tier (warm run)

16 vCPU tier (Ubuntu 22.04): 22 jobs

Runner Minutes, all jobs once vs Blacksmith (total) Median job vs Blacksmith Cost, all jobs once Projected weekly cost Projected weekly minutes
blacksmith 16vcpu 67 1.00× 1.00× $2.16 $717 22,393
github 149 2.20× 1.77× $0.00 $0 49,045
c8a 4cpu 76 1.13× 1.21× $0.24 $84 25,362
c8a 8cpu 65 0.97× 1.12× $0.36 $124 21,960
c8a 16cpu 66 0.98× 1.07× $0.80 $279 22,266

Last week these 22 jobs ran 7,940 times for 26,292 minutes on blacksmith 16vcpu.

4 vCPU tier (Ubuntu 24.04): 22 jobs

Runner Minutes, all jobs once vs Blacksmith (total) Median job vs Blacksmith Cost, all jobs once Projected weekly cost Projected weekly minutes
blacksmith 4vcpu 44 1.00× 1.00× $0.35 $126 15,809
github 125 2.86× 2.49× $0.00 $0 45,023
c8a 2cpu 64 1.45× 1.50× $0.09 $34 23,140
c8a 4cpu 44 1.02× 1.01× $0.13 $47 16,030
c8a 8cpu 38 0.87× 0.85× $0.19 $70 13,672

Last week these 22 jobs ran 8,077 times for 34,250 minutes on blacksmith 4vcpu.

Queue to start, and spot against on-demand

Runner Jobs Median queued (s) p90 queued (s) Spot On-demand
blacksmith 16vcpu 22 6 7 – –
blacksmith 4vcpu 22 7 7 – –
c8a 16cpu 22 47 63 12 10
c8a 2cpu 22 38 57 21 1
c8a 4cpu 44 49 70 31 13
c8a 8cpu 44 42 58 35 9
github 44 2 2 – –

Per job, warm run (minutes; RunsOn cost in brackets)

16 vCPU tier (Ubuntu 22.04)
Workflow Job blacksmith 16vcpu github c8a 4cpu c8a 8cpu c8a 16cpu
CRAP gate (crates) CRAP (stack-auth) 0.5 2.0 0.6 ($0.002) 0.6 ($0.004) 0.6 ($0.007)
CRAP gate (crates) CRAP (stack-encrypt) 0.5 2.6 0.7 ($0.002) 0.6 ($0.004) 0.5 ($0.016)
Mutants Mutants gate 11.6 37.5 20.6 ($0.042) 13.9 ($0.053) 10.7 ($0.073)
Test EQL Build test archive 7.6 17.3 9.3 ($0.039) 8.9 ($0.031) 9.2 ($0.143)
Test EQL Encrypted-domain codegen 0.5 0.8 0.6 ($0.005) 0.6 ($0.008) 0.6 ($0.016)
Test EQL JSON Schema validation 0.6 1.5 0.6 ($0.005) 0.6 ($0.004) 0.6 ($0.017)
Test EQL Matrix coverage inventory 4.0 10.0 5.2 ($0.023) 4.1 ($0.036) 4.2 ($0.070)
Test EQL Rust workspace crates 1.4 2.4 1.8 ($0.010) 1.7 ($0.016) 1.6 ($0.032)
Test EQL SQL doc validation 0.6 0.8 0.7 ($0.002) 0.7 ($0.004) 0.7 ($0.008)
Test EQL Shard PG17 1/4 6.9 11.8 4.1 ($0.009) 4.1 ($0.015) 5.1 ($0.036)
Test EQL Shard PG17 2/4 6.1 9.0 4.0 ($0.009) 4.0 ($0.015) 5.2 ($0.038)
Test EQL Shard PG17 3/4 5.7 11.2 3.9 ($0.008) 4.0 ($0.015) 4.8 ($0.035)
Test EQL Shard PG17 4/4 5.5 5.0 4.1 ($0.009) 3.7 ($0.014) 4.7 ($0.034)
Test EQL Supabase splinter 0.7 1.2 0.9 ($0.006) 0.9 ($0.011) 0.9 ($0.022)
Test EQL Validate (Postgres 17) 0.8 1.3 1.0 ($0.006) 1.0 ($0.011) 1.1 ($0.023)
Test EQL doc anchor links 0.1 0.2 0.2 ($0.002) 0.2 ($0.003) 0.2 ($0.006)
Test EQL e2e property suite (fresh encryption) 8.4 11.4 9.4 ($0.040) 9.4 ($0.075) 9.0 ($0.152)
Test EQL eql_v3 self-containment 0.6 1.0 0.7 ($0.005) 0.6 ($0.009) 0.6 ($0.017)
Test EQL known-failure markers 0.1 0.2 0.2 ($0.002) 0.2 ($0.003) 0.3 ($0.006)
Tests (Go) WASI check, guests and Go (Linux) 2.8 6.8 3.9 ($0.008) 3.1 ($0.012) 3.0 ($0.023)
Tests (crates) fmt, clippy, nextest, doctests, rustdoc 1.3 9.4 2.2 ($0.005) 1.5 ($0.007) 1.4 ($0.013)
Tests (crates) node bindings, napi typings, stack-auth-wasm 1.1 5.3 1.2 ($0.003) 1.1 ($0.006) 1.2 ($0.011)
4 vCPU tier (Ubuntu 24.04)
Workflow Job blacksmith 4vcpu github c8a 2cpu c8a 4cpu c8a 8cpu
Fuzz (crates) fuzz regression (access-key) 1.5 1.8 2.4 ($0.004) 1.5 ($0.004) 1.2 ($0.006)
Fuzz (crates) fuzz regression (check-record) 1.3 2.3 2.0 ($0.003) 1.4 ($0.004) 1.0 ($0.005)
Fuzz (crates) fuzz regression (client-key) 1.2 1.9 1.9 ($0.003) 1.2 ($0.003) 0.9 ($0.005)
Fuzz (crates) fuzz regression (jwt-decode) 1.6 2.9 2.4 ($0.006) 1.4 ($0.004) 1.2 ($0.006)
Fuzz (crates) fuzz regression (plan-build) 1.2 2.2 2.0 ($0.003) 1.4 ($0.004) 1.0 ($0.005)
Fuzz (crates) fuzz regression (sealed-value) 1.2 2.3 2.0 ($0.003) 1.3 ($0.008) 1.0 ($0.006)
Fuzz (crates) fuzz regression (term-decode) 1.2 2.0 2.0 ($0.003) 1.3 ($0.007) 1.1 ($0.012)
Integration — Drizzle (EQL v3) Drizzle v3 integration (db=postgres) 4.5 15.4 5.4 ($0.007) 4.2 ($0.009) 3.8 ($0.014)
Integration — Drizzle (EQL v3) Drizzle v3 integration (db=supabase) 4.1 15.1 5.5 ($0.007) 4.3 ($0.019) 4.0 ($0.015)
Integration — Supabase (EQL v3) Supabase v3 integration (db=supabase) 2.4 7.1 3.4 ($0.005) 2.7 ($0.006) 2.4 ($0.010)
Integration — prisma-next (EQL v3) prisma-next v3 integration (db=postgres) 2.0 6.0 2.8 ($0.004) 2.0 ($0.005) 1.8 ($0.008)
Integration — prisma-next (EQL v3) prisma-next v3 integration (db=supabase) 1.9 5.1 3.0 ($0.004) 2.2 ($0.005) 2.0 ($0.008)
Integration — protect-ffi (native + WASM) protect-ffi integration suite 4.1 19.6 6.1 ($0.008) 4.1 ($0.009) 3.6 ($0.014)
Test JS Lint (Biome) 0.2 0.6 0.4 ($0.001) 0.3 ($0.002) 0.4 ($0.004)
Test JS Run E2E Tests 1.2 4.9 1.9 ($0.003) 1.2 ($0.003) 1.0 ($0.005)
Test JS Run Tests (Bun) 1.4 4.2 2.5 ($0.004) 1.6 ($0.008) 1.2 ($0.006)
Test JS Run Tests (Node 22) 3.4 9.4 5.1 ($0.007) 3.4 ($0.007) 3.0 ($0.013)
Test JS Run Tests (Node 24) 3.6 9.1 5.0 ($0.007) 3.2 ($0.007) 2.7 ($0.024)
Test JS Run WASM E2E Tests (Deno) 2.5 9.1 3.7 ($0.005) 2.7 ($0.006) 2.3 ($0.009)
Tests (Go) Go lint 0.6 1.1 0.8 ($0.002) 0.6 ($0.002) 0.5 ($0.004)
Tests (Go) Go live tests and stack-encrypt examples 1.4 1.7 1.6 ($0.003) 1.3 ($0.004) 1.2 ($0.006)
Unused dependencies (crates) Check unused Rust dependencies 1.2 1.4 1.7 ($0.003) 1.1 ($0.003) 0.8 ($0.005)

@auxesis

auxesis commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Indicative runner costs from this trial

Every successful Mutants gate and Test JS job on this PR across all four commits (including re-runs), grouped by runner option. Time is the median job duration; cost is the average per job.

Mutants gate

Runner Jobs Median time Cost per job
GitHub standard (ubuntu-22.04) 5 28:47 free (public repo)
Blacksmith 16 vCPU 5 11:38 $0.288
RunsOn c8a 4 CPU 5 20:30 $0.025
RunsOn c8a 8 CPU 5 13:52 $0.032
RunsOn c8a 16 CPU 5 10:27 $0.049

Node tests (Node 22 + Node 24)

Runner Jobs Median time Cost per job
GitHub standard (ubuntu-latest) 10 6:28 free (public repo)
Blacksmith 4 vCPU 10 3:39 $0.033
RunsOn 2 CPU (any family) 4 9:09 $0.008
RunsOn 4 CPU (any family) 4 5:39 $0.009
RunsOn 8 CPU (any family) 4 4:17 $0.014
RunsOn c8a 2 CPU 6 5:02 $0.005
RunsOn c8a 4 CPU 6 3:19 $0.006
RunsOn c8a 8 CPU 6 2:54 $0.014

Other Test JS jobs (median time / cost per job)

Runner E2E Bun WASM E2E (Deno) Lint (Biome)
GitHub standard 3:34 / free 4:05 / free 8:28 / free 0:29 / free
Blacksmith 4 vCPU 1:12 / $0.016 1:32 / $0.016 2:49 / $0.024 0:16 / $0.008
RunsOn c8a 2 CPU 1:52 / $0.002 2:29 / $0.003 5:06 / $0.005 0:24 / $0.001
RunsOn c8a 4 CPU 1:15 / $0.003 1:33 / $0.007* 3:38 / $0.007 0:20 / $0.002
RunsOn c8a 8 CPU 0:56 / $0.004 1:09 / $0.006 3:11 / $0.011 0:22 / $0.003

* One of the two jobs fell back to an on-demand instance, which costs about 2.4× the spot price.

Takeaways

  • Mutants gate: RunsOn c8a 16 CPU is the fastest option, and costs about a sixth of Blacksmith 16 vCPU. RunsOn c8a 8 CPU is 2 minutes slower than Blacksmith 16 vCPU at about a ninth of the cost.
  • Node tests: RunsOn c8a 4 CPU matches Blacksmith 4 vCPU on speed (3:19 against 3:39) at about a fifth of the cost. Pinning the c8a family is noticeably faster than letting RunsOn pick any family at the same CPU count.
  • Short jobs (E2E, Bun, Lint): RunsOn c8a 2–4 CPU costs a fraction of a cent per job. Blacksmith's per-minute rounding makes its cost per job on these jobs several times higher.
  • GitHub standard runners are free for public repos but 2–4× slower than the fastest alternative.
  • Fixed cost not included above: the RunsOn stack costs roughly $40–60 a month whether or not jobs run (mostly the NAT gateway, Fargate control plane and WAF).

How these were calculated

  • RunsOn: actual instance type and spot or on-demand lifecycle from RunsOn's job records. The cost covers each instance's launch through to job completion, including boot time, at the average us-west-2 spot price for that instance type between 02:30 and 06:30 UTC on 20 October, or the on-demand list price where a job fell back. It includes the 80 GB gp3 volume. AWS Cost Explorer hasn't published data for today yet, so these are calculated figures rather than billed ones.
  • Blacksmith: list price of $0.004/min per 2 vCPU, with each job rounded up to the whole minute.
  • GitHub: standard runners are free for public repos. For comparison, a 4-core larger runner lists at $0.012/min. At that rate the Mutants gate median would cost about $0.35 per job, and the Node tests about $0.08.
  • These come from 2–10 jobs per option, and the Mutants workload changed between commits, so treat them as indicative.

Adds c8g at 16 and 8 cpu to the Mutants gate and at 4 and 8 cpu to
each Test JS job (Node 22, Node 24, E2E, Bun, WASM E2E, Lint), beside
the c8a x64 entries of the same sizes, so arm64 can be compared on
speed and cost one CPU against another. The family is pinned to c8g
alone for the same reason c8a is.

The images keep each job's Ubuntu release (ubuntu22-full-arm64 for
Mutants, ubuntu24-full-arm64 for Test JS). Nothing else needed to
change for the caches to stay apart by architecture: rust-cache, the
mise cache and the protect-ffi index.node cache all put the runner's
architecture in their keys, and the protect-ffi WASM cache holds
wasm32 output, which is the same on either host.

workflow-mise-cache-image now pins that an arm64 RunsOn label names
its Ubuntu release like an x64 one, and that one prefix serves both.

Claude-Session: https://claude.ai/code/session_017GMWYNGir7bpQacaUzN84U
family=c8g is a prefix match, so RunsOn also launched c8gd (local
NVMe) and c8gn (network-optimised) instances for those entries: of
the arm64 jobs on b52c3d6, the 16 cpu Mutants entry ran on c8g.4xlarge
and c8gd.4xlarge, and the 4 cpu Test JS entries on c8g, c8gd and c8gn.
That mixes families into a comparison meant to be c8g against c8a.
RunsOn's wildcard form, family=c8g.*, matches the c8g family alone.

Claude-Session: https://claude.ai/code/session_017GMWYNGir7bpQacaUzN84U
@auxesis

auxesis commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

arm64 (Graviton) results: c8g against c8a

Commits b52c3d6 and dd4df4d added AWS Graviton4 runners, RunsOn family c8g (arm64), to the Mutants gate and to every Test JS job. Each c8g entry sits beside the c8a (AMD, x64) entry of the same size, so the two compare directly:

  • Mutants: c8g at 16 and 8 cpu.
  • Test JS: c8g at 4 and 8 cpu.

Every option, x64 and arm64, has 3 successful runs on dd4df4d. Every arm64 job there ran on a plain c8g instance.

Summary

  • arm64 is slower for both workflows. Mutants takes about twice as long on c8g. The Test JS jobs take 1.1–1.5× as long.
  • arm64 also costs more per job, almost everywhere. c8g is cheaper per minute, but not by enough to make up for the longer runs. The cleanest comparison is Mutants at 16 cpu, where both ran on spot instances only: c8a cost $0.0068 a minute and c8g $0.0055, so c8g is 18% cheaper per minute. Mutants still costs 1.66× as much per run on c8g.
  • Mutants is where the gap is largest. cargo-mutants tests one mutant at a time, so the speed of each core decides the run time, and Graviton4 is behind AMD Turin on that. RunsOn's own EC2 benchmark gives c8g.large a single-thread score of 1,945, against 3,802 for c8a.large.
  • Recommendation: stay on c8a x64 for these jobs.

Median of 3 runs per option

Workflow Job Size c8a x64 (min) c8g arm64 (min) arm64 time c8a x64 cost c8g arm64 cost arm64 cost
Mutants Mutants gate 16cpu 10.6 21.4 2.03× $0.0716 $0.1185 1.66×
Mutants Mutants gate 8cpu 13.9 26.4 1.89× $0.0533 $0.0888 1.67×
Test JS Run Tests (Node 22) 4cpu 3.5 4.4 1.29× $0.0074 $0.0091 1.23×
Test JS Run Tests (Node 22) 8cpu 2.8 4.0 1.45× $0.0121 $0.0149 1.23×
Test JS Run Tests (Node 24) 4cpu 3.4 4.4 1.29× $0.0074 $0.0094 1.27×
Test JS Run Tests (Node 24) 8cpu 2.7 3.6 1.33× $0.0119 $0.0136 1.14×
Test JS Run E2E Tests 4cpu 1.2 1.6 1.25× $0.0033 $0.0040 1.21×
Test JS Run E2E Tests 8cpu 1.0 1.3 1.31× $0.0056 $0.0062 1.11×
Test JS Run Tests (Bun) 4cpu 1.5 2.0 1.35× $0.0038 $0.0049 1.29×
Test JS Run Tests (Bun) 8cpu 1.1 1.5 1.28× $0.0065 $0.0064 0.98×
Test JS Run WASM E2E Tests (Deno) 4cpu 2.6 4.0 1.52× $0.0061 $0.0084 1.38×
Test JS Run WASM E2E Tests (Deno) 8cpu 2.2 3.4 1.52× $0.0103 $0.0130 1.26×
Test JS Lint (Biome) 4cpu 0.3 0.4 1.10× $0.0018 $0.0018 1.00×
Test JS Lint (Biome) 8cpu 0.3 0.4 1.28× $0.0037 $0.0033 0.89×
  • Times run from job start to job finish, so they exclude time waiting in the queue.
  • Costs are RunsOn's own estimate, printed at the end of each job: EC2 plus the 80 GB disk, billed per second. Each job paid the spot or on-demand price it actually got. Both 16 cpu options ran on spot only. The other sizes were a mix of spot and on-demand.

What arm64 needed

No fixes for architecture. No arm64 job failed because of the CPU architecture. The caches already keep arm64 and x64 apart, because their keys include the architecture:

  • pnpm: node-cache-Linux-arm64-…
  • The protect-ffi native build: ffi-native-Linux-ARM64-…
  • The Rust cache: v0-rust-mutants-Linux-arm64-…
  • mise: adds linux-<arch> to its key itself.
  • The protect-ffi WASM build: shared between architectures on purpose, because WASM output is the same on both.

Two changes that were not architecture fixes:

  1. Pinning the family exactly (dd4df4d). family=c8g matches as a prefix, so RunsOn also launched c8gd (local NVMe disk) and c8gn (faster network) instances on b52c3d6. family=c8g.* matches c8g alone. The 3 runs per option were redone on dd4df4d, and only those runs are in the table.
  2. The workflow-mise-cache-image guard test. It already reads image=ubuntu22-full-arm64 as Ubuntu 22.04, so its logic did not change. New test cases pin that behaviour, and accept a matrix that mixes x64 and arm64 entries under one cache prefix.

Other incidents during these runs (not arm64)

  • A flaky test on GitHub's runner. In one b52c3d6 run, Run Tests (Node 24, github) failed with Hook timed out in 10000ms in oidc-federation-strategy.test.ts. That job runs on GitHub's x64 runner. It passed when re-run.

  • A hung RunsOn job. SQL doc validation (runs-on c8a 4cpu) in Test EQL stayed "in progress" on a RunsOn runner for over 2 hours. It normally takes under a minute. I cancelled the run and re-ran the job, and it passed. I don't know why it hung: a job that never finishes has no log to read, and I have no AWS access to look at the instance (i-059ab4713d12ebdee). A spot interruption is possible but not confirmed.

    The trial jobs set no timeout-minutes, so a hang like this can hold a PR for up to 6 hours. A timeout on each job would make it fail fast instead. That change is not made in this PR.

@auxesis

auxesis commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: arm64 (Graviton) results

This compares the arm64 entries against everything else, using only the runs where both architectures ran side by side: commits b52c3d61 and dd4df4d5, three attempts each. The workload is the same for every runner in these tables. The method is the same as in the earlier comment.

Short version: Graviton (c8g) is slower than c8a at the same CPU count and costs about the same or more per job. Stay on x64 c8a.

Mutants gate

Runner Jobs Median time Cost per job
GitHub standard 6 45:21 free (public repo)
Blacksmith 16 vCPU 6 12:27 $0.421
RunsOn c8a 4 CPU 6 20:36 $0.035
RunsOn c8a 8 CPU 6 13:56 $0.046
RunsOn c8a 16 CPU 6 10:34 $0.085
RunsOn c8g 8 CPU arm64 6 26:41 $0.081
RunsOn c8g 16 CPU arm64 6 20:16 $0.107

On every one of the six attempts, arm64 took about twice as long as c8a at the same CPU count (c8g 16 CPU: 18:52 to 25:19; c8a 16 CPU: 10:29 to 10:36). Every attempt is consistent, so cold caches don't explain it. Graviton4 runs at 2.8 GHz against 4.5 GHz for AMD Turin (c8a). In the two availability zones this stack uses, Graviton spot is only about 20% cheaper, which doesn't make up for taking twice as long.

Test JS (median time / cost per job)

Runner Node tests (22 + 24) E2E Bun WASM E2E (Deno) Lint (Biome)
GitHub standard 6:06 / free 2:03 / free 2:40 / free 5:30 / free 0:30 / free
Blacksmith 4 vCPU 3:36 / $0.034 1:18 / $0.016 1:32 / $0.016 2:48 / $0.026 0:16 / $0.008
RunsOn c8a 2 CPU 4:57 / $0.007 1:57 / $0.003 2:32 / $0.004 3:38 / $0.007 0:25 / $0.002
RunsOn c8a 4 CPU 3:24 / $0.010 1:15 / $0.003 1:31 / $0.005 2:39 / $0.007 0:22 / $0.004
RunsOn c8a 8 CPU 2:49 / $0.014 0:57 / $0.006 1:10 / $0.011 2:13 / $0.020 0:20 / $0.005
RunsOn c8g 4 CPU arm64 4:39 / $0.012 1:34 / $0.005 2:04 / $0.005 4:00 / $0.012 0:23 / $0.002
RunsOn c8g 8 CPU arm64 3:53 / $0.019 1:18 / $0.007 1:29 / $0.007 3:22 / $0.020 0:24 / $0.004

At the same CPU count, arm64 is 20–50% slower on everything except Lint, for about the same or higher cost.

Recommendation

  • Test JS: RunsOn c8a 4 CPU. It's as fast as Blacksmith 4 vCPU on everything except Lint, at about a third of the cost or less (half for Lint).
  • Mutants gate: RunsOn c8a 8 CPU costs about one ninth of Blacksmith 16 vCPU and takes 1.5 minutes longer. RunsOn c8a 16 CPU is the fastest option overall, at about one fifth of Blacksmith's cost.
  • Don't use arm64 for these jobs. It might only become worthwhile if the RunsOn stack used more availability zones, where Graviton spot is cheaper, and even then it wouldn't be enough for Mutants.
  • Add a fallback family: use family=c8a+m8a. 1–4 jobs per option fell back to on-demand instances (about 2.4× the spot price), on both c8a and c8g. m8a has the same CPU as c8a, so adding it gives RunsOn more spot capacity without changing speed.

Notes

  • With family=c8g.*, RunsOn sometimes chose c8gd or c8gn. Their spot prices are within a few percent of c8g.
  • RunsOn costs: spot prices are averaged over 02:30–13:00 UTC on 20 October, for availability zones us-west-2a and us-west-2b only. Jobs that fell back to on-demand are priced at the on-demand list price.
  • Blacksmith: $0.004/min per 2 vCPU, rounded up per job. GitHub: free for public repos.
  • AWS hasn't published billed costs for these days yet, so these figures are calculated, not billed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant