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.

StatisticWhat it answersWhat it hides
PeakDid any instant hit the ceilingA one second spike reads the same as a five minute plateau
SustainedDid a rolling window average stay near the ceilingThe length of the window has to be quoted with the number
Mean over job durationHow much of the reserved capacity the job used in totalA 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.gz

The 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.

StepDurationCores busyCPU utilization of 16 vCPU
actions/checkout0.5 minabout 16 percent
make -j16 bundle4 minabout 1275 percent
gzip -95 min16.25 percent
upload.sh0.5 minunder 1under 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:

  1. Cut the wall clock of the single-threaded step, for example by replacing gzip -9 with a parallel compressor, which raises the mean without changing the size.
  2. 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.

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.