Fastest GitHub Actions runners: disk I/O.
fio inside the job workspace of 93 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.
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.
Blacksmith and Namespace lead x64 QD1 read, within 5% (82,138 and 78,677).tmpfs for scale: 1,154,342 on RunsOn r8a.2xlarge · tmpfs.
Blacksmith and Namespace lead x64 synced writes, within 5% (18,934 and 18,160).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Blacksmith and Namespace lead x64 2 vCPU QD1 read, within 5% (78,754 and 77,442).tmpfs for scale: 1,154,342 on RunsOn r8a.2xlarge · tmpfs.
Blacksmith and Namespace lead x64 2 vCPU synced writes, within 5% (18,223 and 17,813).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest x64 4 vCPU QD1 read: Blacksmith (82,138), ahead of Namespace (77,281).tmpfs for scale: 1,154,342 on RunsOn r8a.2xlarge · tmpfs.
Blacksmith and Namespace lead x64 4 vCPU synced writes, within 5% (18,224 and 17,744).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Blacksmith and Namespace lead x64 8 vCPU QD1 read, within 5% (81,150 and 78,677).tmpfs for scale: 1,154,342 on RunsOn r8a.2xlarge · tmpfs.
Blacksmith and Namespace lead x64 8 vCPU synced writes, within 5% (18,934 and 18,160).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: 685,593 on RunsOn r9g.xlarge · tmpfs.
Fastest arm64 synced writes: RunsOn m9gd.2xlarge · NVMe (17,500), ahead of Warpbuild (8,582).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,564).tmpfs for scale: 685,593 on RunsOn r9g.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 (24,897), ahead of Namespace (19,715).tmpfs for scale: 685,593 on RunsOn r9g.xlarge · tmpfs.
Fastest arm64 4 vCPU synced writes: RunsOn m9gd.xlarge · NVMe (13,696), ahead of Blacksmith (7,280).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,778).tmpfs for scale: 685,593 on RunsOn r9g.xlarge · tmpfs.
Fastest arm64 8 vCPU synced writes: RunsOn m9gd.2xlarge · NVMe (17,500), 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 82,138 times a second on Blacksmith 4 vCPU and 1,425 on RunsOn m8azn.xlarge · gp3 1000 MiB/s, 58× apart; p99 latency is 14 µs against 1,020 µs.
- Synced 4k writes on x64 run from 498 per second on RunsOn m8i.2xlarge · EBS gp3 (workspace mounted nobarrier) to 18,934 on Blacksmith 8 vCPU (workspace mounted nobarrier).
- r8a.large with tmpfs ($0.0010/min at spot) answers 1,105,119 QD1 reads and 424,773 synced writes per second: 91× and 32× the best local NVMe at 2 vCPU (i7i.large, $0.0010) and 599× and 785× default EBS gp3 (r8a.large, $0.0010); its workspace is 23 GiB, from RAM. (EC2 storage suite) What that does to real builds.
More findings22: arm64, NVMe vs EBS, provisioned gp3, tmpfs, storage classes
- 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 79 µs against 971 µs.
- Synced 4k writes on arm64 run from 537 per second on RunsOn c8g.large · EBS gp3 (workspace mounted nobarrier) to 17,500 on RunsOn m9gd.2xlarge · local NVMe.
- Same Xeon 6975P-C CPU, different disk: m8id.2xlarge on local NVMe answers 10,804 QD1 reads and 7,522 synced writes per second, m8i.2xlarge on EBS gp3 1,539 and 498.
- Same Xeon 6975P-C CPU, different disk: m8id.large on local NVMe answers 11,093 QD1 reads and 5,545 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,015 QD1 reads and 7,898 synced writes per second, m8i-flex.xlarge on EBS gp3 1,707 and 527.
- Same Neoverse-V3 CPU, different disk: m9gd.2xlarge on local NVMe answers 13,508 QD1 reads and 17,500 synced writes per second, m9g.2xlarge on EBS gp3 1,810 and 542.
- Same Neoverse-V3 CPU, different disk: m9gd.xlarge on local NVMe answers 13,052 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 go (1,817 → 1,425 IOPS).
- tmpfs on m8a.2xlarge is not a disk at all: 1,153,230 QD1 reads and 427,176 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,114 synced writes per second, from RAM. The workspace is 46 GiB, sized by memory.
- tmpfs on m9g.xlarge is not a disk at all: 682,673 QD1 reads and 303,641 synced writes per second, from RAM. The workspace is 23 GiB, sized by memory.
- tmpfs on r8a.2xlarge is not a disk at all: 1,154,342 QD1 reads and 430,563 synced writes per second, from RAM. The workspace is 92 GiB, sized by memory.
- tmpfs on r8a.xlarge is not a disk at all: 1,109,976 QD1 reads and 431,794 synced writes per second, from RAM. The workspace is 46 GiB, sized by memory.
- tmpfs on r9g.large is not a disk at all: 634,536 QD1 reads and 300,213 synced writes per second, from RAM. The workspace is 23 GiB, sized by memory.
- tmpfs on r9g.xlarge is not a disk at all: 685,593 QD1 reads and 301,758 synced writes per second, from RAM. The workspace is 46 GiB, sized by memory.
- tmpfs: 635k–1.2M QD1 reads and 300k–432k synced writes per second across 8 runners (8 RunsOn configurations).
- VM disk: 5.8k–82k QD1 reads and 926–19k synced writes per second across 43 runners from 7 providers (6 Avrea runners, 6 Blacksmith runners, 3 GitHub runners, 12 Namespace runners, 2 StarSling x64 runners, 9 Ubicloud runners and 5 Warpbuild runners).
- local NVMe: 11k–14k QD1 reads and 5.5k–18k synced writes per second across 9 runners (9 RunsOn configurations).
- network disk: 6.7k–8.2k QD1 reads and 3.4k–4.4k synced writes per second across 2 runners (2 GitHub runners).
- EBS: 1.4k–1.8k QD1 reads and 498–564 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.
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 | 769,525 | 253,803 | 6.5 GiB/s | 2.0 GiB/s | ||
| 8 vCPU | 13 µs | 625,138 | 328,609 | 6.5 GiB/s | 2.0 GiB/s | ||
| 2 vCPU | 17 µs | 780,194 | 190,198 | 7.1 GiB/s | 2.1 GiB/s | ||
| Namespace 8x32 | 12 µs | 1,106,358 | 536,857 | 4.1 GiB/s | 2.9 GiB/s | ||
| 8x16 | 13 µs | 1,109,002 | 538,065 | 4.1 GiB/s | 2.9 GiB/s | ||
| 4x16 | 13 µs | 994,526 | 433,713 | 4.1 GiB/s | 2.9 GiB/s | ||
| 4x8 | 13 µs | 992,094 | 429,133 | 4.1 GiB/s | 2.9 GiB/s | ||
| 2x8 | 13 µs | 350,533 | 222,814 | 3.9 GiB/s | 2.8 GiB/s | ||
| StarSling | 17 µs | 231,481 | 40,717 | 2.4 GiB/s | 2.7 GiB/s | ||
| 4 vCPU | 18 µs | 243,587 | 42,697 | 3.9 GiB/s | 3.3 GiB/s | ||
| Avrea | 17 µs | 377,847 | 303,429 | 12.0 GiB/s | 7.2 GiB/s | ||
| 8 vCPU | 19 µs | 296,449 | 147,459 | 6.6 GiB/s | 3.3 GiB/s | ||
| 4 vCPU | 20 µs | 439,265 | 224,464 | 7.3 GiB/s | 5.5 GiB/s | ||
| Warpbuild | 48 µs | 163,423 | 50,132 | 7.9 GiB/s | 5.1 GiB/s | ||
| 4 vCPU | 64 µs | 134,981 | 37,360 | 6.7 GiB/s | 5.1 GiB/s | ||
| 2 vCPU | 59 µs | 152,215 | 53,614 | 7.0 GiB/s | 4.5 GiB/s | ||
| RunsOn i7i.2xlarge | 293 µs | 300,465 | 165,111 | 2.0 GiB/s | 1.5 GiB/s | ||
| m8azn.3xlarge | 807 µs | 2,997 | 2,993 | 401 MiB/s | 401 MiB/s | ||
| c8a.2xlarge | 856 µs | 2,995 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m8a.2xlarge | 905 µs | 2,994 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| m8a.2xlarge | 872 µs | 15,996 | 15,992 | 1,001 MiB/s | 1,001 MiB/s | ||
| m8a.2xlarge | <1 µs | 4,255,814 | 3,787,945 | 18.1 GiB/s | 9.9 GiB/s | ||
| r8a.2xlarge | <1 µs | 4,231,165 | 3,828,376 | 18.5 GiB/s | 10.1 GiB/s | ||
| m8azn.xlarge | 872 µs | 2,994 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| m8azn.xlarge | 1,020 µs | 15,994 | 15,992 | 1,001 MiB/s | 1,001 MiB/s | ||
| m8i.2xlarge | 1,036 µs | 2,993 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| m8i-flex.2xlarge | 893 µs | 2,993 | 2,995 | 401 MiB/s | 401 MiB/s | ||
| m8a.xlarge | 848 µs | 2,994 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| m8id.2xlarge | 239 µs | 134,258 | 67,071 | 905 MiB/s | 432 MiB/s | ||
| c8a.xlarge | 799 µs | 2,995 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| r8a.xlarge | <1 µs | 4,213,197 | 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 | ||
| i7i.xlarge | 301 µs | 150,053 | 82,489 | 1,010 MiB/s | 788 MiB/s | ||
| m8id.xlarge | 217 µ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 | ||
| r8a.large | 799 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m8i.xlarge | 832 µs | 2,992 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| m8a.large | 774 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| r8a.large | <1 µs | 2,206,009 | 1,943,626 | 18.1 GiB/s | 9.9 GiB/s | ||
| c8a.large | 741 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m8id.large | 216 µs | 33,542 | 16,752 | 226 MiB/s | 108 MiB/s | ||
| m8i.large | 872 µs | 2,992 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| m8i-flex.large | 881 µs | 2,994 | 2,993 | 401 MiB/s | 401 MiB/s | ||
| i7i.large | 313 µs | 75,022 | 41,229 | 503 MiB/s | 395 MiB/s | ||
| t8i.medium | 807 µs | 3,016 | 2,998 | 401 MiB/s | 401 MiB/s | ||
| Ubicloud premium | 136 µs | 374,058 | 22,032 | 3.8 GiB/s | 1.7 GiB/s | ||
| standard 8 vCPU | 469 µs | 251,230 | 13,012 | 2.4 GiB/s | 716 MiB/s | ||
| premium 4 vCPU | 569 µs | 169,052 | 10,804 | 2.4 GiB/s | 537 MiB/s | ||
| standard 4 vCPU | 528 µs | 189,378 | 12,781 | 2.5 GiB/s | 578 MiB/s | ||
| standard 2 vCPU | 1,106 µs | 105,928 | 60,814 | 2.5 GiB/s | 622 MiB/s | ||
| premium 2 vCPU | 1,597 µs | 129,551 | 64,617 | 2.6 GiB/s | 849 MiB/s | ||
| GitHub | 202 µs | 38,946 | 18,331 | 784 MiB/s | 532 MiB/s | ||
| 4-core | 289 µs | 19,578 | 9,322 | 394 MiB/s | 247 MiB/s | ||
| 2-core | 222 µs | 9,378 | 9,098 | 199 MiB/s | 199 MiB/s | ||
| AWS CodeBuild medium | 725 µs | 2,990 | 2,970 | 252 MiB/s | 252 MiB/s | ||
| large | 782 µs | 2,993 | 2,975 | 252 MiB/s | 251 MiB/s | ||
| small | 823 µs | 2,988 | 2,963 | 252 MiB/s | 252 MiB/s | ||
| arm64 | |||||||
| Warpbuild xfast (Apple M4 Pro) | 79 µs | 340,451 | 5,369 | 20.1 GiB/s | 27.0 GiB/s | ||
| xfast (Apple M4 Pro) 12 vCPU | 53 µs | 326,216 | 4,460 | 21.1 GiB/s | 25.1 GiB/s | ||
| 8 vCPU | 717 µs | 4,994 | 4,993 | 401 MiB/s | 401 MiB/s | ||
| 4 vCPU | 823 µ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 | 171,567 | 27,356 | 1.6 GiB/s | 873 MiB/s | ||
| 2 vCPU | 43 µs | 74,915 | 27,167 | 1.6 GiB/s | 823 MiB/s | ||
| Avrea | 53 µs | 139,536 | 16,707 | 36.8 GiB/s | 15.9 GiB/s | ||
| 4 vCPU | 57 µs | 146,579 | 15,160 | 31.5 GiB/s | 16.0 GiB/s | ||
| 2 vCPU | 58 µs | 166,747 | 11,109 | 30.4 GiB/s | 12.7 GiB/s | ||
| Namespace Apple silicon 4x16 | 85 µs | 148,271 | 13,347 | 30.9 GiB/s | 14.8 GiB/s | ||
| Apple silicon 2x8 | 92 µs | 160,201 | 28,352 | 33.8 GiB/s | 14.2 GiB/s | ||
| 8x16 | 90 µs | 188,179 | 46,180 | 965 MiB/s | 746 MiB/s | ||
| 8x32 | 98 µs | 186,558 | 40,849 | 956 MiB/s | 763 MiB/s | ||
| 4x8 | 90 µs | 144,342 | 32,144 | 973 MiB/s | 741 MiB/s | ||
| 4x16 | 90 µs | 141,434 | 32,119 | 964 MiB/s | 750 MiB/s | ||
| 2x8 | 95 µs | 79,835 | 24,244 | 941 MiB/s | 696 MiB/s | ||
| RunsOn m9gd.2xlarge | 81 µs | 174,573 | 87,209 | 1.1 GiB/s | 561 MiB/s | ||
| m9g.2xlarge | <1 µs | 2,676,737 | 2,518,860 | 14.1 GiB/s | 9.7 GiB/s | ||
| m9g.2xlarge | 766 µs | 2,995 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| c8g.2xlarge | 946 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m9gd.xlarge | 112 µs | 87,221 | 43,590 | 585 MiB/s | 281 MiB/s | ||
| m9g.xlarge | 971 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m9g.xlarge | <1 µs | 2,737,595 | 2,553,373 | 15.7 GiB/s | 10.1 GiB/s | ||
| r9g.xlarge | <1 µs | 2,723,360 | 2,518,108 | 14.7 GiB/s | 9.3 GiB/s | ||
| c8g.xlarge | 987 µs | 2,994 | 2,993 | 401 MiB/s | 401 MiB/s | ||
| m9gd.large | 81 µs | 43,607 | 21,783 | 293 MiB/s | 141 MiB/s | ||
| r9g.large | <1 µs | 1,218,242 | 1,203,310 | 13.2 GiB/s | 8.5 GiB/s | ||
| m9g.large | 766 µs | 2,993 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| c8g.large | 954 µs | 2,993 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| Ubicloud standard | 334 µs | 93,227 | 39,464 | 937 MiB/s | 576 MiB/s | ||
| standard 8 vCPU | 358 µs | 223,613 | 117,930 | 889 MiB/s | 451 MiB/s | ||
| standard 4 vCPU | 594 µs | 173,246 | 46,361 | 798 MiB/s | 428 MiB/s | ||
| GitHub | 206 µs | 19,579 | 12,039 | 394 MiB/s | 337 MiB/s | ||
| 2-core | 232 µs | 9,383 | 5,717 | 199 MiB/s | 143 MiB/s | ||
- IOPS are 4k blocks, medians of finished jobs
- bold best disk measured on the arch (tmpfs is RAM, left out); its bar is tinted once it has 3 runs
- , : 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.