vCPU

A vCPU is the virtual processor a cloud instance exposes to its guest operating system, usually one hardware thread of a core. Why build tools count them.

A vCPU (virtual CPU) is one virtual processor that a cloud instance exposes to its guest operating system, appearing as a single entry in the processor list the guest can schedule threads onto. On most x86 instances a vCPU is one hardware thread of a physical core, so a machine advertised with 4 vCPU commonly holds 2 cores that run 2 threads each.

The number matters in GitHub Actions because build tools read it. Compilers, test runners, and task graphs size their worker pools from the processor count they detect at startup, so the vCPU count of the machine a job lands on sets the parallelism of that job even when nobody configured a worker count.

Definition

A hypervisor divides one physical host among several guests. The guest kernel sees a set of logical processors and schedules threads onto them, and the hypervisor maps each of those logical processors onto time on the physical hardware. Each logical processor presented to the guest is a vCPU. It is an allocation unit for scheduling, and the cloud provider bills and quotas by it.

The mapping from vCPU to hardware follows simultaneous multithreading (SMT), which lets several threads run concurrently on one physical core. AWS documents each thread as a vCPU on the instance and gives m5.xlarge as the worked case: two CPU cores with two threads per core, for four vCPUs in total (Amazon EC2 CPU options, checked on 2026-08-13). Google Cloud uses the same model and states that each hardware thread is called a virtual CPU, adding that for Arm processors Compute Engine uses one thread per core and each vCPU maps to a physical core with no SMT (Compute Engine CPU platforms, checked on 2026-08-13).

Both halves of that rule show up in the same catalog:

Instance shapePhysical coresThreads per corevCPUs reported
m5.xlarge, x86 with SMT on224
m5.xlarge launched with threads per core set to 1212
m6g.large, AWS Graviton212
Arm machine type on Compute Enginen1n

Core and thread counts are from AWS's supported CPU options table, checked on 2026-08-13, where every Graviton family lists a default of 1 thread per core.

What the number does not tell you

A vCPU count is a quantity with no quality attached. It carries no clock speed, no processor generation, no cache size, and no memory bandwidth, so two machines advertising 8 vCPU can finish the same compile in different times.

It also hides topology. Two vCPUs that are sibling threads on one core share that core's execution units and caches, so saturating both delivers less throughput than two threads on two separate cores. An Arm instance with 4 vCPU and no SMT holds four full cores, while an x86 instance with 4 vCPU and SMT holds two.

Reading the count from inside a job

Several numbers are available inside a running job, and they answer different questions.

nproc prints the processing units available to the current process, which can be fewer than the online processors when an affinity mask has pinned the process to a subset. /proc/cpuinfo lists every logical processor the kernel sees. In Node, the documentation says os.cpus().length should not be used to calculate available parallelism and points at os.availableParallelism() instead (Node os module, checked on 2026-08-13). That function wraps libuv's uv_available_parallelism(), which on Linux inspects the calling thread's CPU affinity mask to see whether it has been pinned (libuv miscellaneous utilities, checked on 2026-08-13).

A CPU quota is a separate mechanism from an affinity mask. Docker's --cpus="1.5" guarantees a container at most one and a half CPUs of the host (Docker resource constraints, checked on 2026-08-13), and it does that by capping CPU time rather than by shortening the processor list. A tool that counts entries in that list therefore reads the host's number and starts one worker per entry while the job holds a much smaller share of real CPU time.

Example

This workflow never mentions a worker count. The parallelism comes entirely from the vCPU count of whatever machine claims the job.

name: build
on:
  push:
    branches: [main]

jobs:
  compile:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: nproc
      - run: make -j"$(nproc)"
      - run: npx jest

GitHub documents its standard hosted Linux runner for private repositories as 2 processors with 8 GB of RAM, and the same label on a public repository as 4 processors with 16 GB (GitHub-hosted runners reference, checked on 2026-08-13). The file above therefore behaves differently on the two shapes without a single line changing.

On the private-repository shape, nproc prints 2, make runs two recipes at a time, and Jest starts one worker, because its documented default in a single run is the number of cores available minus one for the main thread (Jest CLI options, checked on 2026-08-13). On the public-repository shape the same commands give four recipes and three workers.

What that means on a shared runner

Now put the job in a container or a slice that holds a quota rather than a dedicated machine. The tool reads the processor list, which still describes the host, and sizes its pool from a number the job cannot use. A pool of 63 Jest workers on a 2 vCPU budget runs 63 module graphs in memory while the scheduler hands out two cores of time, so the job gets slower and the memory ceiling is the first thing to break.

Pinning the number turns parallelism into a property of the workflow:

jobs:
  compile:
    runs-on: ubuntu-latest
    env:
      MAKEFLAGS: -j2
    steps:
      - uses: actions/checkout@v4
      - run: make
      - run: npx jest --maxWorkers=50%

Jest documents the percentage form for environments where the CPU count varies, which keeps the pool proportional to the machine instead of fixed. The defaults worth knowing before pinning anything:

ToolDefault parallelismWhere to set it
GNU makeserial, and unlimited when -j is passed with no number-j<N> or the MAKEFLAGS environment variable
Jestcores minus one in a single run, half of the cores in watch mode--maxWorkers=<num> or --maxWorkers=<percent>%
Node code calling os.cpus().lengthevery logical processor the host reportsos.availableParallelism(), which honors the affinity mask

Once the worker counts are explicit, a change in runner size becomes a deliberate edit in two places rather than a silent change in behavior.

FAQ

What is a vCPU?

A vCPU is one virtual processor a cloud instance exposes to the guest operating system, appearing as a single entry in the processor list the guest schedules work onto. On most x86 instances one vCPU is one hardware thread of a physical core, so a 4 vCPU machine is commonly 2 cores running 2 threads each.

Is a vCPU the same as a physical CPU core?

Usually not on x86. AWS documents each thread of an SMT-capable core as a vCPU, so an m5.xlarge with 2 cores and 2 threads per core reports 4 vCPUs. On Arm hardware the mapping is one to one: AWS Graviton instance types default to 1 thread per core, and Compute Engine documents one thread per core for Arm processors.

Why does my build tool spawn more workers than the runner has vCPUs?

Most tools size their worker pool from the processor list they read at startup. A cgroup CPU quota limits how much CPU time the process receives without shortening that list, so a tool can read the host's processors and start one worker per entry while the job holds a much smaller share. Pin the worker count with a flag instead.

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.