Fastest GitHub Actions runners: disk I/O.
fio inside the job workspace of 90 runners from 9 providers, 2 to 12 vCPU. These are the disk's limits. A build goes through the Linux page cache and often never reaches them, so a large gap here can be no gap on a build.
How this shows up in real builds
Latency per provider
Each provider's best disk, RunsOn once per storage (the Storage pill). QD1 is one 4k read at a time: what a database or a cold read pays. Most build steps batch their I/O through the page cache. tmpfs is RAM, not a disk: add it to see the gap on the same scale.
Fastest x64 QD1 read: Blacksmith (84,602), ahead of Namespace (77,750).tmpfs for scale: 1,156,647 on RunsOn r8a.2xlarge · tmpfs.
Fastest x64 synced writes: Blacksmith (20,425), ahead of Namespace (17,899).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest x64 2 vCPU QD1 read: Namespace (77,442), ahead of Blacksmith (66,914).tmpfs for scale: 1,156,647 on RunsOn r8a.2xlarge · tmpfs.
Blacksmith and Namespace lead x64 2 vCPU synced writes, within 5% (18,196 and 17,899).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest x64 4 vCPU QD1 read: Blacksmith (84,602), ahead of Namespace (77,727).tmpfs for scale: 1,156,647 on RunsOn r8a.2xlarge · tmpfs.
Fastest x64 4 vCPU synced writes: Blacksmith (20,425), ahead of Namespace (17,822).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Namespace and Blacksmith lead x64 8 vCPU QD1 read, within 5% (77,750 and 76,986).tmpfs for scale: 1,156,647 on RunsOn r8a.2xlarge · tmpfs.
Fastest x64 8 vCPU synced writes: Blacksmith (19,717), ahead of Namespace (17,821).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Warpbuild and Blacksmith lead arm64 QD1 read, within 5% (26,620 and 26,166).tmpfs for scale: 680,179 on RunsOn m9g.xlarge · tmpfs.
Fastest arm64 synced writes: RunsOn m9gd.2xlarge · NVMe (17,418), ahead of Warpbuild (8,765).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest arm64 2 vCPU QD1 read: Blacksmith (23,450), ahead of Avrea (17,388).tmpfs for scale: 680,179 on RunsOn m9g.xlarge · tmpfs.
Fastest arm64 2 vCPU synced writes: RunsOn m9gd.large · NVMe (7,267), ahead of Blacksmith (6,384).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest arm64 4 vCPU QD1 read: Blacksmith (25,272), ahead of Namespace (19,715).tmpfs for scale: 680,179 on RunsOn m9g.xlarge · tmpfs.
Fastest arm64 4 vCPU synced writes: RunsOn m9gd.xlarge · NVMe (13,696), ahead of Blacksmith (7,376).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest arm64 8 vCPU QD1 read: Blacksmith (26,166), ahead of Avrea (19,050).tmpfs for scale: 680,179 on RunsOn m9g.xlarge · tmpfs.
Fastest arm64 8 vCPU synced writes: RunsOn m9gd.2xlarge · NVMe (17,418), ahead of Blacksmith (7,834).volatile and nobarrier mounts make each flush cheaper by making it less durable.
What the numbers say
Extremes on x64, and RAM against disk.
- On x64, one small read at a time (4k, QD1) comes back 84,602 times a second on Blacksmith 4 vCPU and 1,571 on RunsOn m8i.2xlarge · EBS gp3, 54× apart; p99 latency is 14 µs against 1,036 µs.
- Synced 4k writes on x64 run from 498 per second on RunsOn m8i.2xlarge · EBS gp3 (workspace mounted nobarrier) to 20,425 on Blacksmith 4 vCPU (workspace mounted nobarrier).
- r8a.xlarge with tmpfs ($0.0020/min at spot) answers 1,109,976 QD1 reads and 430,233 synced writes per second: 91× and 36× the best local NVMe at 4 vCPU (i7i.xlarge, $0.0019) and 607× and 797× default EBS gp3 (m8a.xlarge, $0.0015); its workspace is 46 GiB, from RAM. (EC2 storage suite) What that does to real builds.
More findings21: EBS, arm64, NVMe vs EBS, provisioned gp3, tmpfs, storage classes
- Every EBS-backed workspace (3 AWS CodeBuild x64 runners, 25 RunsOn configurations and 3 Warpbuild configurations) answers between 1,560 and 1,844 QD1 reads and 498 to 563 synced writes per second, whoever runs it and whatever throughput it is provisioned for: each request is a round trip to network storage.
- On arm64, one small read at a time (4k, QD1) comes back 26,620 times a second on Warpbuild xfast (Apple M4 Pro) 6 vCPU and 1,560 on RunsOn m9g.xlarge · EBS gp3, 17× apart; p99 latency is 70 µs against 938 µs.
- Synced 4k writes on arm64 run from 537 per second on RunsOn c8g.large · EBS gp3 (workspace mounted nobarrier) to 17,418 on RunsOn m9gd.2xlarge · local NVMe.
- Same Xeon 6975P-C CPU, different disk: m8id.2xlarge on local NVMe answers 10,957 QD1 reads and 7,657 synced writes per second, m8i.2xlarge on EBS gp3 1,571 and 498.
- Same Xeon 6975P-C CPU, different disk: m8id.large on local NVMe answers 11,021 QD1 reads and 5,565 synced writes per second, m8i-flex.large on EBS gp3 1,745 and 532.
- Same Xeon 6975P-C CPU, different disk: m8id.xlarge on local NVMe answers 11,064 QD1 reads and 7,974 synced writes per second, m8i-flex.xlarge on EBS gp3 1,707 and 531.
- Same Neoverse-V3 CPU, different disk: m9gd.2xlarge on local NVMe answers 13,464 QD1 reads and 17,418 synced writes per second, m9g.2xlarge on EBS gp3 1,788 and 542.
- Same Neoverse-V3 CPU, different disk: m9gd.xlarge on local NVMe answers 12,823 QD1 reads and 13,696 synced writes per second, m9g.xlarge on EBS gp3 1,560 and 543.
- Same Neoverse-V3 CPU, different disk: m9gd.large on local NVMe answers 13,492 QD1 reads and 7,267 synced writes per second, m9g.large on EBS gp3 1,788 and 542.
- Provisioning gp3 at 1000 MiB/s on m8a.2xlarge takes sequential reads from 402 MiB/s to 1,002 MiB/s (2.5×); QD1 reads barely move (1,791 → 1,792 IOPS): it buys bandwidth, not latency.
- Provisioning gp3 at 1000 MiB/s on m8azn.xlarge takes sequential reads from 401 MiB/s to 1,001 MiB/s (2.5×); QD1 reads barely move (1,815 → 1,764 IOPS): it buys bandwidth, not latency.
- tmpfs on m8a.2xlarge is not a disk at all: 1,156,332 QD1 reads and 428,514 synced writes per second, from RAM. The workspace is 46 GiB, sized by memory.
- tmpfs on m9g.2xlarge is not a disk at all: 664,023 QD1 reads and 301,438 synced writes per second, from RAM. The workspace is 46 GiB, sized by memory.
- tmpfs on m9g.xlarge is not a disk at all: 680,179 QD1 reads and 303,641 synced writes per second, from RAM. The workspace is 15 GiB, sized by memory.
- tmpfs on r8a.2xlarge is not a disk at all: 1,156,647 QD1 reads and 430,802 synced writes per second, from RAM. The workspace is 92 GiB, sized by memory.
- tmpfs on r9g.xlarge is not a disk at all: 658,442 QD1 reads and 300,910 synced writes per second, from RAM. The workspace is 46 GiB, sized by memory.
- tmpfs: 658k–1.2M QD1 reads and 301k–431k synced writes per second across 6 runners (6 RunsOn configurations).
- VM disk: 6.0k–85k QD1 reads and 578–20k synced writes per second across 42 runners from 7 providers (6 Avrea runners, 6 Blacksmith runners, 3 GitHub runners, 11 Namespace runners, 2 StarSling x64 runners, 9 Ubicloud runners and 5 Warpbuild runners).
- local NVMe: 11k–13k QD1 reads and 5.6k–17k synced writes per second across 9 runners (9 RunsOn configurations).
- network disk: 6.0k–8.9k QD1 reads and 3.1k–4.6k synced writes per second across 2 runners (2 GitHub runners).
- EBS: 1.6k–1.8k QD1 reads and 498–563 synced writes per second across 31 runners from 3 providers (3 AWS CodeBuild x64 runners, 25 RunsOn configurations and 3 Warpbuild configurations).
Reading these numbers
fio measures what a disk can do. Whether a job gets there depends on the job.
At 4 vCPU, Blacksmith answers 46× the QD1 reads of RunsOn m8a.xlarge · EBS gp3 (84,602 against 1,828 per second), and the Rust build takes 1m 59s on one and 1m 55s on the other. The Docker build is where the disk shows: 5m 19s against 9m 05s. Every build, per runner.
What each column says about a job
- QD1 reads and synced writes: databases, SQLite-backed tests and anything that flushes each write. Every flush waits for the storage, and a network disk pays a round trip each time.
-
Random writes at depth and sequential throughput:
docker buildlayers, package installs and cache archives. They batch their writes and run into the disk's ceiling, not its latency. - None of them: compile jobs. A compiler waits for the CPU, and it reads its files back from the page cache.
Every runner
Each provider's best disk first; its other machines sit behind +N more. Bars are log scale.
| One request at a time | 4k, 32 in flight | Sequential, 1M | |||||
|---|---|---|---|---|---|---|---|
| Runner | QD1 read ↑ | p99 ↓ | Sync write ↑ | Read ↑ | Write ↑ | Read ↑ | Write ↑ |
| x64 | |||||||
| Blacksmith | 14 µs | 856,257 | 253,803 | 6.3 GiB/s | 2.1 GiB/s | ||
| 8 vCPU | 13 µs | 683,187 | 328,609 | 6.5 GiB/s | 1.9 GiB/s | ||
| 2 vCPU | 17 µs | 672,377 | 173,541 | 7.1 GiB/s | 2.1 GiB/s | ||
| Namespace 8x32 | 13 µs | 1,106,358 | 536,857 | 4.1 GiB/s | 2.9 GiB/s | ||
| 8x16 | 13 µs | 1,109,002 | 476,969 | 4.1 GiB/s | 2.9 GiB/s | ||
| 4x16 | 12 µs | 1,003,666 | 406,763 | 4.1 GiB/s | 2.9 GiB/s | ||
| 4x8 | 13 µs | 1,009,155 | 418,913 | 4.1 GiB/s | 2.9 GiB/s | ||
| 2x8 | 13 µs | 351,146 | 222,814 | 4.1 GiB/s | 2.9 GiB/s | ||
| StarSling | 17 µs | 265,725 | 30,327 | 2.5 GiB/s | 2.2 GiB/s | ||
| 4 vCPU | 17 µs | 227,686 | 34,742 | 3.9 GiB/s | 2.8 GiB/s | ||
| Avrea | 17 µs | 454,066 | 303,429 | 10.6 GiB/s | 7.0 GiB/s | ||
| 8 vCPU | 19 µs | 235,683 | 147,459 | 6.6 GiB/s | 3.3 GiB/s | ||
| 4 vCPU | 20 µs | 468,080 | 246,705 | 6.8 GiB/s | 4.0 GiB/s | ||
| Warpbuild | 48 µs | 163,423 | 50,132 | 7.9 GiB/s | 5.1 GiB/s | ||
| 4 vCPU | 64 µs | 124,074 | 38,883 | 8.7 GiB/s | 5.3 GiB/s | ||
| 2 vCPU | 59 µs | 176,232 | 68,626 | 7.1 GiB/s | 4.5 GiB/s | ||
| RunsOn i7i.large | 220 µs | 75,023 | 41,229 | 503 MiB/s | 395 MiB/s | ||
| m8azn.3xlarge | 799 µs | 2,997 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| c8a.2xlarge | 840 µs | 2,995 | 2,990 | 401 MiB/s | 401 MiB/s | ||
| r8a.2xlarge | <1 µs | 4,255,216 | 3,830,379 | 18.6 GiB/s | 10.1 GiB/s | ||
| m8a.2xlarge | 905 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m8a.2xlarge | 872 µs | 15,996 | 15,990 | 1,001 MiB/s | 1,001 MiB/s | ||
| m8a.2xlarge | <1 µs | 4,258,470 | 3,812,490 | 18.2 GiB/s | 10.0 GiB/s | ||
| m8azn.xlarge | 897 µs | 2,994 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| m8azn.xlarge | 872 µs | 15,994 | 15,991 | 1,001 MiB/s | 1,001 MiB/s | ||
| m8i.2xlarge | 1,036 µs | 2,993 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| i7i.2xlarge | 272 µs | 300,464 | 165,110 | 2.0 GiB/s | 1.5 GiB/s | ||
| m8i-flex.2xlarge | 836 µs | 2,992 | 2,995 | 401 MiB/s | 401 MiB/s | ||
| m8id.2xlarge | 228 µs | 134,258 | 67,071 | 905 MiB/s | 432 MiB/s | ||
| m8a.xlarge | 848 µs | 2,994 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| c8a.xlarge | 832 µs | 2,995 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| r8a.xlarge | <1 µs | 4,266,392 | 3,801,248 | 18.2 GiB/s | 10.0 GiB/s | ||
| m8azn.large | 856 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m8id.xlarge | 215 µs | 67,089 | 33,525 | 451 MiB/s | 215 MiB/s | ||
| m8i-flex.xlarge | 848 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m8i.xlarge | 889 µs | 2,992 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| i7i.xlarge | 301 µs | 150,053 | 82,488 | 1,010 MiB/s | 788 MiB/s | ||
| m8a.large | 692 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| r8a.large | 799 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| c8a.large | 807 µs | 2,994 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| m8id.large | 224 µs | 33,542 | 16,753 | 226 MiB/s | 108 MiB/s | ||
| m8i-flex.large | 881 µs | 2,994 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| m8i.large | 872 µs | 2,992 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| t8i.medium | 807 µs | 3,016 | 2,998 | 401 MiB/s | 401 MiB/s | ||
| Ubicloud premium | 136 µs | 374,058 | 28,158 | 3.5 GiB/s | 1.4 GiB/s | ||
| standard 8 vCPU | 469 µs | 251,230 | 13,012 | 2.4 GiB/s | 944 MiB/s | ||
| premium 4 vCPU | 569 µs | 173,146 | 16,062 | 2.3 GiB/s | 537 MiB/s | ||
| standard 4 vCPU | 700 µs | 189,160 | 11,680 | 2.5 GiB/s | 506 MiB/s | ||
| standard 2 vCPU | 1,106 µs | 123,524 | 60,814 | 2.7 GiB/s | 806 MiB/s | ||
| premium 2 vCPU | 1,597 µs | 147,821 | 64,867 | 2.6 GiB/s | 867 MiB/s | ||
| GitHub | 202 µs | 38,940 | 18,331 | 784 MiB/s | 538 MiB/s | ||
| 4-core | 289 µs | 19,560 | 11,459 | 394 MiB/s | 247 MiB/s | ||
| 2-core | 255 µs | 9,378 | 9,098 | 199 MiB/s | 199 MiB/s | ||
| AWS CodeBuild small | 823 µs | 2,989 | 2,963 | 252 MiB/s | 252 MiB/s | ||
| large | 815 µs | 2,993 | 2,972 | 252 MiB/s | 252 MiB/s | ||
| medium | 750 µs | 2,989 | 2,970 | 252 MiB/s | 252 MiB/s | ||
| arm64 | |||||||
| Warpbuild xfast (Apple M4 Pro) | 70 µs | 340,451 | 5,906 | 20.8 GiB/s | 27.2 GiB/s | ||
| xfast (Apple M4 Pro) 12 vCPU | 52 µs | 326,216 | 4,587 | 21.1 GiB/s | 25.9 GiB/s | ||
| 8 vCPU | 717 µs | 4,993 | 4,994 | 401 MiB/s | 401 MiB/s | ||
| 4 vCPU | 840 µs | 4,193 | 4,194 | 302 MiB/s | 302 MiB/s | ||
| 2 vCPU | 823 µs | 4,194 | 4,193 | 302 MiB/s | 301 MiB/s | ||
| Blacksmith | 33 µs | 259,773 | 29,073 | 1.6 GiB/s | 993 MiB/s | ||
| 4 vCPU | 39 µs | 180,949 | 28,040 | 1.6 GiB/s | 873 MiB/s | ||
| 2 vCPU | 43 µs | 74,915 | 27,167 | 1.6 GiB/s | 823 MiB/s | ||
| Namespace Apple silicon | 71 µs | 151,102 | 17,731 | 31.2 GiB/s | 15.4 GiB/s | ||
| 8x16 | 89 µs | 188,957 | 52,820 | 965 MiB/s | 748 MiB/s | ||
| 8x32 | 98 µs | 186,558 | 40,849 | 956 MiB/s | 785 MiB/s | ||
| 4x8 | 90 µs | 144,201 | 38,014 | 988 MiB/s | 739 MiB/s | ||
| 4x16 | 90 µs | 141,434 | 34,460 | 978 MiB/s | 738 MiB/s | ||
| 2x8 | 95 µs | 79,239 | 29,910 | 941 MiB/s | 696 MiB/s | ||
| Avrea | 53 µs | 139,536 | 16,621 | 36.8 GiB/s | 15.6 GiB/s | ||
| 4 vCPU | 59 µs | 146,579 | 13,857 | 29.3 GiB/s | 15.3 GiB/s | ||
| 2 vCPU | 58 µs | 166,284 | 14,993 | 30.4 GiB/s | 12.7 GiB/s | ||
| RunsOn m9gd.large | 81 µs | 43,606 | 21,783 | 293 MiB/s | 141 MiB/s | ||
| m9gd.2xlarge | 84 µs | 174,573 | 87,209 | 1.1 GiB/s | 561 MiB/s | ||
| m9g.2xlarge | <1 µs | 2,654,165 | 2,507,590 | 14.3 GiB/s | 9.2 GiB/s | ||
| m9g.2xlarge | 807 µs | 2,995 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| c8g.2xlarge | 848 µs | 2,994 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| r9g.xlarge | <1 µs | 2,646,436 | 2,497,143 | 13.9 GiB/s | 9.0 GiB/s | ||
| m9gd.xlarge | 129 µs | 87,221 | 43,590 | 585 MiB/s | 280 MiB/s | ||
| m9g.xlarge | <1 µs | 2,723,038 | 2,553,373 | 15.5 GiB/s | 9.5 GiB/s | ||
| m9g.xlarge | 938 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| c8g.xlarge | 987 µs | 2,994 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| m9g.large | 766 µs | 2,993 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| c8g.large | 905 µs | 2,993 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| Ubicloud standard | 330 µs | 222,352 | 117,930 | 889 MiB/s | 434 MiB/s | ||
| standard 4 vCPU | 371 µs | 170,555 | 26,833 | 823 MiB/s | 428 MiB/s | ||
| standard 2 vCPU | 334 µs | 93,715 | 39,464 | 949 MiB/s | 608 MiB/s | ||
| GitHub | 200 µs | 19,595 | 12,039 | 394 MiB/s | 337 MiB/s | ||
| 2-core | 212 µs | 9,377 | 6,017 | 199 MiB/s | 164 MiB/s | ||
- IOPS are 4k blocks, medians of finished jobs
- bold best disk measured on the arch (tmpfs is RAM, left out)
- , : cheaper, less durable flushes
Pick the disk per job on RunsOn
-
The instance type sets the storage under the workspace: EBS gp3 by default, provisioned gp3 for bandwidth, local NVMe
on the
dandifamilies, or tmpfs when the job fits in RAM. Pick it with thefamilylabel; each one, measured: EC2 storage benchmark. - What tmpfs does to real builds, what it costs and where it does nothing: Disks? Where we're going, we don't need disks.
How it's measured
fio runs inside the job, in GITHUB_WORKSPACE: the disk a build writes to, on the filesystem the provider
mounted there. Every runner is compared, whatever its shape.
fio settingsblock sizes, queue depths, mounts
- QD1 random read: 4k blocks, one request in flight; IOPS and p99 latency.
- Sync write: 4k writes, each followed by
fdatasync. - Random read and write: 4k blocks, 4 jobs with 32 requests in flight each.
- Sequential read and write: 1M blocks, 32 requests in flight.
- The workspace filesystem, its size and its mount options come from the job's own mount table.
- Earlier versions of this page used a different harness and other fio settings on 2 vCPU runners; those numbers are gone rather than mixed in.