Runner Utilization
Runner utilization is the share of a runner's CPU, memory, disk, and network that a job actually uses while it holds the machine, and how to measure it.
Runner utilization is the share of a runner's provisioned resources that a job actually consumes while it holds the machine, measured per resource across the life of the job: CPU, memory, filesystem space, disk throughput, and network throughput. Each figure is a percentage of what was allocated, so a job can pass every step while using a small fraction of the machine it reserved.
The number matters in GitHub Actions because a runner is held for the whole job. One runner runs one job at a time and stays occupied from the first step to the last, so the machine is paid for and unavailable for its full wall clock duration whether a step saturates it or leaves it idle.
Definition
Utilization is defined per resource, and each resource has its own ceiling.
CPU utilization is consumed CPU time divided by available CPU time over the same interval. A 16 vCPU machine makes 16 CPU-seconds available for every second of wall clock, so a process burning 4 CPU-seconds per second sits at 25 percent. Values above 100 percent for a single process are possible in tools that report per-core figures, which is why the denominator has to be stated.
Memory utilization is peak resident memory divided by provisioned memory. Peak is the figure that matters, because the process that gets killed is the one that asked for a page when none was free.
Filesystem utilization is peak bytes used on the volume divided by volume size. It is the one dimension where the ceiling produces an immediate hard failure rather than a slowdown.
Disk and network utilization are throughput measures. Both are read against the throughput the underlying volume or instance supports, so the same megabytes per second reads as saturation on one shape and as an idle link on another.
Peak, sustained, and mean
A single percentage is ambiguous until the statistic behind it is named, and three statistics are in common use.
| Statistic | What it answers | What it hides |
|---|---|---|
| Peak | Did any instant hit the ceiling | A one second spike reads the same as a five minute plateau |
| Sustained | Did a rolling window average stay near the ceiling | The length of the window has to be quoted with the number |
| Mean over job duration | How much of the reserved capacity the job used in total | A saturated step averaged against a long idle tail |
Peak alone justifies a bigger machine too easily, because every dependency install spikes. Mean alone justifies a smaller machine too easily, because it averages the one step that needed the cores against the steps that did not. Reading the pair together is what makes the number actionable.
Where the raw numbers come from
Utilization is measured on the runner by sampling operating system counters while the job runs. On Linux the counters are cumulative and live in the kernel interfaces: /proc/stat for CPU time, /proc/meminfo for memory, and the cgroup v2 files for a job confined to a slice, where cpu.stat reports usage_usec and memory.peak records the maximum memory usage recorded for the cgroup (cgroup v2 kernel documentation, checked on 2026-08-13). Utilization is derived from those counters by dividing a delta by the elapsed interval and the allocated capacity.
A single step can be measured without any agent. GNU time reports both halves of the picture for one command, printing "Percent of CPU this job got" and "Maximum resident set size" under the verbose flag (GNU time, checked on 2026-08-13). That percentage is relative to one core, so a value near 100 on a multi-core machine describes a single-threaded workload.
Example
This job packages a release. It asks for a 16 vCPU machine because one step compiles in parallel, and it then holds that machine through a step that can only ever use one core.
name: release
on:
push:
tags: ['v*']
jobs:
package:
runs-on: warp-ubuntu-latest-x64-16x
steps:
- uses: actions/checkout@v4
- run: /usr/bin/time -v make -j"$(nproc)" bundle
- run: gzip -9 dist/bundle.tar
- run: ./scripts/upload.sh dist/bundle.tar.gzThe gzip step is the one that sets the shape of the whole reading. GNU gzip compresses a single stream on a single thread, so it occupies one of the sixteen vCPUs while the other fifteen stay idle: 1 divided by 16, or 6.25 percent CPU utilization for as long as that step runs.
Put a duration against each step and the job-level numbers fall out of arithmetic.
| Step | Duration | Cores busy | CPU utilization of 16 vCPU |
|---|---|---|---|
actions/checkout | 0.5 min | about 1 | 6 percent |
make -j16 bundle | 4 min | about 12 | 75 percent |
gzip -9 | 5 min | 1 | 6.25 percent |
upload.sh | 0.5 min | under 1 | under 5 percent |
The job holds the machine for 10 minutes, which reserves 160 vCPU-minutes (16 vCPU multiplied by 10 minutes). The steps consume roughly 0.5 + 48 + 5 + 0.1, or 53.6 vCPU-minutes. Mean CPU utilization is therefore about 34 percent against a peak sustained figure of 75 percent, and half the wall clock is spent at 6.25 percent.
Memory tells a separate story on the same run. If the compile peaks at 3.1 GB resident on a machine with 64 GB, peak memory utilization is under 5 percent, and no amount of CPU tuning moves that number.
Reading the two numbers together
The gap between 75 percent peak and 34 percent mean is the actionable part. It says the machine was sized for one step and paid for during three, so there are two directions to take:
- Cut the wall clock of the single-threaded step, for example by replacing
gzip -9with a parallel compressor, which raises the mean without changing the size. - Split the job in two, so the parallel compile runs on the large machine and the packaging and upload run on a small one.
Both moves are visible in the same measurement afterwards, which is why utilization is worth recording per job rather than sampled by hand. Running the same job on an 8 vCPU machine instead shows the other side: the compile step stretches to about 6 minutes while the single-threaded step stays at 5, so the job runs about 12 minutes and mean CPU utilization rises to roughly 56 percent of a smaller reservation.
One caution on interpretation. Utilization describes a single runner while it holds a job. It leaves out how long the job waited in the queue before it started, and how many runners in a fleet were busy at the same moment. Those are separate measurements with separate fixes.
Related Terms
- Right sizing GitHub Actions runners: how to turn measured peak and mean utilization into a runner size decision, step by step.
- vCPU: what one virtual processor is, and why the count sets the parallelism of build tools that read it at startup.
- How to detect an undersized GitHub Actions runner: the symptoms that separate a machine that is too small from a workflow that is badly ordered.
- Runner observability documentation: where per-job CPU, memory, filesystem, disk IO, and network utilization are reported.
- Job reports documentation: per-job duration, queue time, CPU, and memory at the 75th and 90th percentiles across runs.
- WarpBuild pricing: per minute rates by runner type.
FAQ
What is runner utilization?
Runner utilization is the share of a runner's provisioned resources that a job consumes while it holds the machine, reported per resource: CPU, memory, filesystem, disk throughput, and network. Each figure is a percentage of what was allocated, measured across the life of the job.
What is a good CPU utilization number for a GitHub Actions job?
There is no single target, because the useful reading is the gap between peak and mean. A job whose peak sustained CPU sits near the ceiling is limited by the machine, and a job whose mean stays low for most of its wall clock is holding cores that no step can use. Both readings point at a size change rather than a code change.
Why is my job slow when CPU utilization is low?
Low CPU utilization means the bottleneck lives somewhere else. The usual candidates are a single-threaded step that can only occupy one core, a step waiting on network transfer such as a dependency download or an image pull, and a step waiting on disk throughput. Memory pressure produces the same shape once swapping starts.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.