Moving Mojibake CI to Namespace

GitHub’s service isn’t top-notch these days (incident history), right?

I moved Mojibake’s CI to Namespace to see how it handles a real, awkward matrix like mine: 27 jobs, four operating systems in QEMU, and big-endian emulation. Jobs ran a quarter faster overall, and the macOS test ran three times faster. Getting the BSD and Haiku VMs to boot took three fixes.

Important thing to consider before reading this post:

  1. The one thing holding up the whole PR was the Namespace trial plan’s concurrency limit. The Team plan doubles that to 64 Linux and 24 macOS vCPUs, which should remove most queuing. I haven’t measured it yet.
  2. For 21 of the 25 jobs, I only changed the runner label. The BSD and Haiku VM jobs needed the fixes described below.
  3. Mojibake runs tests also on all BSD OSes + Haiku, which you don’t see every day when Windows + Linux + macOS cover almost everything.

TLDR, the PR is here: https://github.com/zaerl/mojibake/pull/156

Here is what I measured, what I had to fix, and what I’d work on next.

The setup

Every Mojibake PR starts 27 jobs across 4 workflows, and most of them are Linux jobs that are easy to move. See: https://github.com/zaerl/mojibake/tree/main/.github/workflows

  • Test (9 jobs): native, optimized, feature-disabled and sanitizer builds on Linux x86_64 and ARM64, macOS and Windows, plus WASM and a 60-second fuzz run.
  • Platforms (13 jobs): FreeBSD, OpenBSD, NetBSD and Haiku in QEMU VMs; Alpine/musl on x86_64, emulated i386 and big-endian s390x; GCC 9 and Clang 10; strict C11; MinGW; Android and iOS cross-compiles.
  • Quality (4 jobs): clang-format, clang-tidy, TypeScript typechecks, and a check that generated files are current.
  • Dependency review (1 job).

The Namespace branch moved 25 of the 27 jobs; only the two Windows jobs stayed on GitHub-hosted runners. GitHub’s CodeQL scanning adds three more jobs that also run on GitHub.

To compare, I rebased the Namespace branch onto the same main commit and ran every workflow three times on each side, within one hour on 4 November. The sides took turns so they never ran at once: the Namespace branch still sends its Windows jobs to GitHub, and overlapping runs would have queued the baseline. The numbers below are medians of the three runs (last main commit, Namespace PR).

The results

Once a job had a machine, Namespace was faster almost everywhere. Compile and test jobs took a third less time, and several, the macOS test among them, more than halved. The VM jobs gained 9%.

Dumbbell chart: median job runtime on GitHub-hosted vs. Namespace runners. 21 of 24 jobs ran faster on Namespace; Linux ARM64, NetBSD and Haiku did not.

Three jobs did not get faster. Linux ARM64 was clearly slower in all three runs. NetBSD and Haiku changed by less than the noise, and the noise is real: the two Windows jobs ran on GitHub-hosted runners in both setups and still differed by 11% in total. I treat any change of 10% or less as no change.

The biggest gain was the macOS test job on a 6×14 Apple Silicon Mac: 47 s against 146 s on GitHub’s macos-latest, with the same 4.6 million assertions passing.

Where the time went

Faster runners didn’t make the PR faster. From the first job queued to the last finished, a PR took a median of 267 s on Namespace vs. 218 s on GitHub because jobs waited for a machine. The BSD and Haiku VMs also needed workarounds.

  1. Queueing. In each run, 12 to 15 of the 27 jobs waited a minute or more before starting, up to 201 seconds. On GitHub, none did, and the longest wait was 43 seconds. Namespace caps concurrency by vCPUs: the Developer plan allows 32 Linux vCPUs, so only 8 of my 4-vCPU runners run at once, while a PR starts 23 Linux jobs there.
  2. ARM64 got slower. The Linux ARM64 test job took 144–162 s on Namespace, compared with 91–109 s on GitHub’s ubuntu-24.04-arm, both with 4 vCPUs.
  3. FreeBSD, OpenBSD, and Haiku needed three fixes. cross-platform-actions passes the SSH key to the VM on a small FAT disk that it builds on the host. On Namespace, that broke three times in a row (my bad…):
    1. fdisk and mkfs.fat were missing, so the job installs them.
    2. losetup failed with Operation not permitted, so these jobs run in a privileged container (namespace-features:container.privileged=true).
    3. mount failed with codepage cp437 not found: the kernel can’t mount FAT and can’t load modules. Small mount and umount shims now write that one disk with mtools, in userspace.

With those fixes, all three VMs boot and pass the full test suite. The shim depends on the action’s internals, so an update to the action could break it.

Three runs per side are enough to separate the big effects from the noise, not to rank jobs that differ by less than 10%.

Does it pay off for an open-source project?

For a public repository, Namespace competes with free GitHub-hosted runners, so the case rests on speed and on what that speed costs.

  • Cost. Mojibake ran CI about 135 times in the last 30 days. With both Mac jobs on Namespace, a run bills about 30 Linux minutes and 2 macOS minutes, roughly $0.25-0.35, or $30–50 a month.
  • Speed. The longest job in all six runs was the Windows MSVC build on GitHub, at 3–4 minutes. On GitHub, it was also the last to finish. On Namespace, queued jobs finished after it. Without the queue, MSVC is the limit again, and Namespace’s Windows runners (Team plan and up) are the lever that could move it.
  • Concurrency. The Developer plan’s 32 vCPUs fit 8 jobs at a time. Team doubles that to 64 vCPUs for $100 a month.

For an open-source project, Namespace’s open-source program is the way in: it sponsors the compute, and the speedup comes free.

What I’d work on

If I were on the team, these are what I’d fix or document first, in the order a new user hits them.

  1. The runner kernel can’t mount FAT. Kernel 7.1.4 has vfat, but not the cp437 codepage, and it can’t load modules, so every FAT mount fails with codepage cp437 not found. cross-platform-actions, a common way to run BSD and Haiku CI, depends on that mount. Building cp437 into the kernel would fix it for everyone.
  2. VM-based actions need privileged mode, and nothing points you there. The runner container has no /dev/loop* devices, and losetup returns Operation not permitted until namespace-features:container.privileged=true is set. A short docs page on running QEMU-based actions would save the trial and error.
  3. Queueing is invisible from GitHub. Jobs waited up to 201 seconds with nothing in the GitHub log saying they were held by the plan’s vCPU cap. The dashboard reports queue time, but someone reading GitHub logs won’t look there. A line in the job log saying “queued: concurrency limit” would explain it on the spot.
  4. One clear regression: ARM64. The Linux ARM64 test job took a median of 158 s against 93 s on GitHub, on the same 4-vCPU shape, in all three runs. NetBSD and Haiku ran about as fast as on GitHub, while 19 of the other 21 jobs were more than 10% faster. I’d want to know why.
  5. Also, a dark mode will be very cool.

What’s next

25 of the 27 jobs now run on Namespace, and all of them have passed there. The next step is to remove the queue and fix the root cause instead of working around it.

  • Ask Namespace for higher limits during the trial, re-run, and show the queue disappearing. Or simply buy it!
  • Propose an mtools fallback to cross-platform-actions upstream, so its resource disk no longer needs a kernel FAT mount.
  • Try Namespace’s Windows runners on the MSVC job, since that job sets how long a PR takes once the queue is gone.

What stayed with me: 21 of the 25 jobs moved with nothing but a new runner label. The macOS runner was three times faster than GitHub’s. And the one hard problem came down to a single missing kernel codepage.

Until next time.

@online{zaerl2026-moving-mojibake-ci-to-namespace,
  author = {Francesco Bigiarini},
  title = {Moving Mojibake CI to Namespace},
  date = {2026-11-04},
  url = {https://zaerl.com/2026/11/04/moving-mojibake-ci-to-namespace/},
  urldate = {2026-11-04}
}