Fastest GitHub Actions runners: disk I/O.
fio inside the job workspace of 94 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.
Fastest x64 QD1 read: Namespace (107,149), ahead of Blacksmith (76,132).tmpfs for scale: 1,153,982 on RunsOn r8a.2xlarge · tmpfs.
Fastest x64 synced writes: Namespace (30,012), ahead of Blacksmith (18,263).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest x64 2 vCPU QD1 read: Namespace (104,659), ahead of Blacksmith (76,132).tmpfs for scale: 1,153,982 on RunsOn r8a.2xlarge · tmpfs.
Fastest x64 2 vCPU synced writes: Namespace (28,582), ahead of Blacksmith (17,760).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest x64 4 vCPU QD1 read: Namespace (105,808), ahead of Blacksmith (72,739).tmpfs for scale: 1,153,982 on RunsOn r8a.2xlarge · tmpfs.
Fastest x64 4 vCPU synced writes: Namespace (30,012), ahead of Blacksmith (18,263).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest x64 8 vCPU QD1 read: Namespace (107,149), ahead of Blacksmith (67,311).tmpfs for scale: 1,153,982 on RunsOn r8a.2xlarge · tmpfs.
Fastest x64 8 vCPU synced writes: Namespace (28,934), ahead of Blacksmith (17,257).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest arm64 QD1 read: Blacksmith (32,691), ahead of Warpbuild (30,673).tmpfs for scale: 669,005 on RunsOn m9g.xlarge · tmpfs.
Fastest arm64 synced writes: RunsOn m9gd.2xlarge · NVMe (17,416), ahead of Warpbuild (9,790).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest arm64 2 vCPU QD1 read: Blacksmith (31,028), ahead of Avrea (20,971).tmpfs for scale: 669,005 on RunsOn m9g.xlarge · tmpfs.
Fastest arm64 2 vCPU synced writes: Blacksmith (8,442), ahead of RunsOn m9gd.large · NVMe (7,267).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest arm64 4 vCPU QD1 read: Blacksmith (32,691), ahead of Avrea (19,824).tmpfs for scale: 669,005 on RunsOn m9g.xlarge · tmpfs.
Fastest arm64 4 vCPU synced writes: RunsOn m9gd.xlarge · NVMe (14,861), ahead of Blacksmith (9,201).volatile and nobarrier mounts make each flush cheaper by making it less durable.
Fastest arm64 8 vCPU QD1 read: Blacksmith (28,610), ahead of Avrea (20,797).tmpfs for scale: 669,005 on RunsOn m9g.xlarge · tmpfs.
Fastest arm64 8 vCPU synced writes: RunsOn m9gd.2xlarge · NVMe (17,416), ahead of Blacksmith (9,092).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 107,149 times a second on Namespace 8x16 and 1,430 on RunsOn m8a.2xlarge · EBS gp3, 75× apart; p99 latency is 10 µs against 995 µs.
- Synced 4k writes on x64 run from 487 per second on RunsOn m8i.2xlarge · EBS gp3 (workspace mounted nobarrier) to 30,012 on Namespace 4x8.
- r8a.large with tmpfs ($0.0009/min at spot) answers 1,107,000 QD1 reads and 428,125 synced writes per second: 90× and 33× the best local NVMe at 2 vCPU (i7i.large, $0.0010) and 612× and 791× default EBS gp3 (c8a.large, $0.0009); its workspace is 14 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 32,691 times a second on Blacksmith 4 vCPU and 1,777 on Warpbuild 2 vCPU, 18× apart; p99 latency is 37 µs against 840 µs.
- Synced 4k writes on arm64 run from 537 per second on RunsOn c8g.large · EBS gp3 (workspace mounted nobarrier) to 17,416 on RunsOn m9gd.2xlarge · local NVMe.
- Same Xeon 6975P-C CPU, different disk: m8id.2xlarge on local NVMe answers 10,966 QD1 reads and 7,472 synced writes per second, m8i.2xlarge on EBS gp3 1,580 and 487.
- Same Xeon 6975P-C CPU, different disk: m8id.large on local NVMe answers 11,102 QD1 reads and 5,555 synced writes per second, m8i-flex.large on EBS gp3 1,767 and 528.
- Same Xeon 6975P-C CPU, different disk: m8id.xlarge on local NVMe answers 10,878 QD1 reads and 7,930 synced writes per second, m8i-flex.xlarge on EBS gp3 1,751 and 524.
- Same Neoverse-V3 CPU, different disk: m9gd.2xlarge on local NVMe answers 13,446 QD1 reads and 17,416 synced writes per second, m9g.2xlarge on EBS gp3 1,812 and 543.
- Same Neoverse-V3 CPU, different disk: m9gd.xlarge on local NVMe answers 13,382 QD1 reads and 14,861 synced writes per second, m9g.xlarge on EBS gp3 1,822 and 543.
- Same Neoverse-V3 CPU, different disk: m9gd.large on local NVMe answers 13,533 QD1 reads and 7,267 synced writes per second, m9g.large on EBS gp3 1,789 and 542.
- Provisioning gp3 at 1000 MiB/s on m8a.2xlarge takes sequential reads from 401 MiB/s to 1,001 MiB/s (2.5×); QD1 reads go (1,587 → 1,776 IOPS).
- 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,648 → 1,771 IOPS): it buys bandwidth, not latency.
- tmpfs on m8a.2xlarge is not a disk at all: 1,135,113 QD1 reads and 428,937 synced writes per second, from RAM. The workspace is 29 GiB, sized by memory.
- tmpfs on m9g.2xlarge is not a disk at all: 653,126 QD1 reads and 300,362 synced writes per second, from RAM. The workspace is 29 GiB, sized by memory.
- tmpfs on m9g.xlarge is not a disk at all: 669,005 QD1 reads and 304,088 synced writes per second, from RAM. The workspace is 14 GiB, sized by memory.
- tmpfs on r8a.2xlarge is not a disk at all: 1,153,982 QD1 reads and 430,608 synced writes per second, from RAM. The workspace is 59 GiB, sized by memory.
- tmpfs on r8a.xlarge is not a disk at all: 1,102,306 QD1 reads and 433,841 synced writes per second, from RAM. The workspace is 29 GiB, sized by memory.
- tmpfs on r9g.large is not a disk at all: 651,023 QD1 reads and 297,673 synced writes per second, from RAM. The workspace is 14 GiB, sized by memory.
- tmpfs on r9g.xlarge is not a disk at all: 642,690 QD1 reads and 302,278 synced writes per second, from RAM. The workspace is 29 GiB, sized by memory.
- tmpfs: 643k–1.2M QD1 reads and 298k–434k synced writes per second across 8 runners (8 RunsOn configurations).
- VM disk: 5.3k–107k QD1 reads and 806–30k synced writes per second across 44 runners from 7 providers (6 Avrea runners, 6 Blacksmith runners, 3 boat x64 runners, 3 GitHub runners, 12 Namespace runners, 9 Ubicloud runners and 5 Warpbuild runners).
- local NVMe: 11k–14k QD1 reads and 5.6k–17k synced writes per second across 9 runners (9 RunsOn configurations).
- network disk: 5.9k–7.3k QD1 reads and 2.9k–4.2k synced writes per second across 2 runners (2 GitHub runners).
- EBS: 1.4k–1.8k QD1 reads and 487–571 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 | |||||||
| Namespace 8x16 | 10 µs | 908,730 | 548,767 | 4.3 GiB/s | 2.8 GiB/s | ||
| 8x32 | 10 µs | 1,095,135 | 563,499 | 5.0 GiB/s | 3.4 GiB/s | ||
| 4x16 | 11 µs | 1,205,486 | 568,824 | 5.0 GiB/s | 3.4 GiB/s | ||
| 4x8 | 10 µs | 1,208,861 | 603,259 | 5.0 GiB/s | 3.4 GiB/s | ||
| 2x8 | 10 µs | 802,042 | 544,907 | 4.4 GiB/s | 2.9 GiB/s | ||
| Blacksmith | 15 µs | 783,458 | 174,903 | 5.3 GiB/s | 2.2 GiB/s | ||
| 8 vCPU | 19 µs | 425,239 | 222,802 | 5.9 GiB/s | 2.0 GiB/s | ||
| 4 vCPU | 16 µs | 746,571 | 220,196 | 6.6 GiB/s | 1.6 GiB/s | ||
| Avrea | 19 µs | 331,271 | 203,674 | 16.5 GiB/s | 11.3 GiB/s | ||
| 4 vCPU | 21 µs | 451,829 | 219,963 | 15.7 GiB/s | 10.8 GiB/s | ||
| 2 vCPU | 14 µs | 553,778 | 261,987 | 15.9 GiB/s | 10.3 GiB/s | ||
| Warpbuild | 29 µs | 191,815 | 67,698 | 4.4 GiB/s | 3.0 GiB/s | ||
| 4 vCPU | 40 µs | 210,099 | 78,934 | 4.3 GiB/s | 3.1 GiB/s | ||
| 2 vCPU | 60 µs | 204,864 | 88,324 | 4.2 GiB/s | 2.7 GiB/s | ||
| RunsOn i7i.large | 285 µs | 75,023 | 41,231 | 503 MiB/s | 395 MiB/s | ||
| m8azn.3xlarge | 840 µs | 2,997 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| c8a.2xlarge | 897 µs | 2,995 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| m8a.2xlarge | <1 µs | 4,232,110 | 3,822,441 | 18.5 GiB/s | 9.9 GiB/s | ||
| m8a.2xlarge | 807 µs | 15,994 | 15,991 | 1,001 MiB/s | 1,001 MiB/s | ||
| r8a.2xlarge | <1 µs | 4,310,049 | 3,831,509 | 18.4 GiB/s | 10.0 GiB/s | ||
| m8a.2xlarge | 995 µs | 2,995 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| m8azn.xlarge | 872 µs | 2,994 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| m8azn.xlarge | 823 µs | 15,994 | 15,991 | 1,001 MiB/s | 1,001 MiB/s | ||
| m8i.2xlarge | 1,020 µs | 2,993 | 2,993 | 401 MiB/s | 401 MiB/s | ||
| m8id.2xlarge | 247 µs | 134,257 | 67,076 | 905 MiB/s | 432 MiB/s | ||
| m8i-flex.2xlarge | 954 µs | 2,992 | 2,995 | 401 MiB/s | 401 MiB/s | ||
| m8a.xlarge | 799 µs | 2,994 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| c8a.xlarge | 938 µs | 2,994 | 2,991 | 401 MiB/s | 401 MiB/s | ||
| i7i.2xlarge | 346 µs | 300,466 | 165,110 | 2.0 GiB/s | 1.5 GiB/s | ||
| r8a.xlarge | <1 µs | 4,201,526 | 3,800,170 | 17.8 GiB/s | 9.9 GiB/s | ||
| m8azn.large | 758 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m8id.xlarge | 247 µs | 67,090 | 33,527 | 451 MiB/s | 216 MiB/s | ||
| m8i-flex.xlarge | 897 µs | 2,993 | 2,993 | 401 MiB/s | 401 MiB/s | ||
| m8i.xlarge | 995 µs | 2,993 | 2,993 | 401 MiB/s | 401 MiB/s | ||
| i7i.xlarge | 261 µs | 150,053 | 82,487 | 1,010 MiB/s | 788 MiB/s | ||
| m8a.large | 872 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| r8a.large | 856 µs | 2,995 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| r8a.large | <1 µs | 2,149,284 | 1,933,449 | 18.3 GiB/s | 10.0 GiB/s | ||
| c8a.large | 741 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m8id.large | 167 µs | 33,541 | 16,753 | 226 MiB/s | 108 MiB/s | ||
| m8i.large | 971 µs | 2,993 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| m8i-flex.large | 889 µs | 2,993 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| t8i.medium | 864 µs | 2,999 | 2,998 | 401 MiB/s | 401 MiB/s | ||
| boat | 206 µs | 466,442 | 165,954 | 10.5 GiB/s | 892 MiB/s | ||
| 8 vCPU | 569 µs | 294,651 | 66,127 | 7.4 GiB/s | 1,000 MiB/s | ||
| 4 vCPU | 331 µs | 406,699 | 99,819 | 9.7 GiB/s | 1.1 GiB/s | ||
| Ubicloud premium | 684 µs | 274,211 | 19,507 | 2.9 GiB/s | 735 MiB/s | ||
| standard 8 vCPU | 856 µs | 255,065 | 20,376 | 2.3 GiB/s | 923 MiB/s | ||
| standard 4 vCPU | 528 µs | 188,948 | 21,529 | 2.5 GiB/s | 694 MiB/s | ||
| premium 4 vCPU | 725 µs | 228,178 | 23,853 | 2.2 GiB/s | 909 MiB/s | ||
| standard 2 vCPU | 1,122 µs | 127,031 | 19,627 | 2.5 GiB/s | 793 MiB/s | ||
| premium 2 vCPU | 2,114 µs | 116,012 | 23,536 | 2.6 GiB/s | 857 MiB/s | ||
| GitHub | 239 µs | 38,975 | 18,243 | 784 MiB/s | 545 MiB/s | ||
| 4-core | 255 µs | 19,574 | 10,796 | 394 MiB/s | 245 MiB/s | ||
| 2-core | 272 µs | 9,375 | 8,843 | 199 MiB/s | 199 MiB/s | ||
| AWS CodeBuild medium | 733 µs | 2,991 | 2,974 | 241 MiB/s | 251 MiB/s | ||
| large | 725 µs | 2,988 | 2,977 | 243 MiB/s | 251 MiB/s | ||
| small | 791 µs | 2,989 | 2,972 | 251 MiB/s | 251 MiB/s | ||
| arm64 | |||||||
| Blacksmith | 37 µs | 249,846 | 35,425 | 1.6 GiB/s | 831 MiB/s | ||
| 8 vCPU | 37 µs | 248,393 | 28,043 | 1.7 GiB/s | 781 MiB/s | ||
| 2 vCPU | 39 µs | 252,404 | 36,713 | 1.7 GiB/s | 857 MiB/s | ||
| Warpbuild xfast (Apple M4 Pro) | 40 µs | 376,960 | 5,311 | 11.7 GiB/s | 10.7 GiB/s | ||
| xfast (Apple M4 Pro) 12 vCPU | 33 µs | 384,762 | 5,596 | 10.5 GiB/s | 11.1 GiB/s | ||
| 8 vCPU | 823 µs | 4,995 | 4,993 | 401 MiB/s | 401 MiB/s | ||
| 4 vCPU | 848 µs | 4,194 | 4,193 | 302 MiB/s | 302 MiB/s | ||
| 2 vCPU | 840 µs | 4,194 | 4,194 | 302 MiB/s | 301 MiB/s | ||
| Avrea | 60 µs | 151,990 | 18,394 | 34.1 GiB/s | 14.8 GiB/s | ||
| 8 vCPU | 56 µs | 143,275 | 12,897 | 47.5 GiB/s | 16.0 GiB/s | ||
| 4 vCPU | 58 µs | 149,169 | 15,117 | 39.9 GiB/s | 15.8 GiB/s | ||
| Namespace Apple silicon 2x8 | 77 µs | 150,754 | 15,029 | 30.6 GiB/s | 11.7 GiB/s | ||
| Apple silicon 4x16 | 105 µs | 145,281 | 21,805 | 32.4 GiB/s | 12.7 GiB/s | ||
| 8x32 | 91 µs | 185,101 | 45,666 | 920 MiB/s | 769 MiB/s | ||
| 8x16 | 95 µs | 183,519 | 36,033 | 926 MiB/s | 700 MiB/s | ||
| 4x8 | 94 µs | 142,059 | 34,657 | 943 MiB/s | 628 MiB/s | ||
| 4x16 | 96 µs | 146,800 | 31,796 | 928 MiB/s | 657 MiB/s | ||
| 2x8 | 103 µs | 77,677 | 25,565 | 924 MiB/s | 672 MiB/s | ||
| RunsOn m9gd.large | 81 µs | 43,609 | 21,783 | 293 MiB/s | 141 MiB/s | ||
| m9gd.2xlarge | 84 µs | 174,573 | 87,208 | 1.1 GiB/s | 561 MiB/s | ||
| m9g.2xlarge | 848 µs | 2,995 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| m9g.2xlarge | <1 µs | 2,615,018 | 2,495,107 | 13.3 GiB/s | 9.1 GiB/s | ||
| c8g.2xlarge | 856 µs | 2,994 | 2,993 | 401 MiB/s | 401 MiB/s | ||
| m9gd.xlarge | 116 µs | 87,221 | 43,589 | 585 MiB/s | 281 MiB/s | ||
| m9g.xlarge | 856 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| r9g.xlarge | <1 µs | 2,611,352 | 2,491,994 | 13.8 GiB/s | 8.9 GiB/s | ||
| m9g.xlarge | <1 µs | 2,695,794 | 2,522,498 | 14.7 GiB/s | 9.4 GiB/s | ||
| c8g.xlarge | 864 µs | 2,992 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| m9g.large | 848 µs | 2,993 | 2,994 | 401 MiB/s | 401 MiB/s | ||
| r9g.large | <1 µs | 1,277,313 | 1,235,007 | 13.8 GiB/s | 9.2 GiB/s | ||
| c8g.large | 856 µs | 2,994 | 2,992 | 401 MiB/s | 401 MiB/s | ||
| Ubicloud standard | 408 µs | 241,173 | 112,271 | 748 MiB/s | 503 MiB/s | ||
| standard 4 vCPU | 498 µs | 150,193 | 35,176 | 742 MiB/s | 394 MiB/s | ||
| standard 2 vCPU | 510 µs | 99,268 | 27,725 | 776 MiB/s | 422 MiB/s | ||
| GitHub | 214 µs | 9,376 | 6,334 | 199 MiB/s | 164 MiB/s | ||
| 4-core | 228 µs | 19,574 | 12,391 | 394 MiB/s | 320 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.