Skip to content
Français
CI/CD & Automatisation medium

GitHub Actions runners: where do your workflows run?

25 min de lecture

Read this page in French

When you trigger a GitHub Actions workflow, where does it actually run? Not on your machine. Not on the GitHub servers hosting your code. It runs on a runner, a machine dedicated to executing your jobs.

Understanding runners matters, because the choice directly affects:

  • the speed of your pipelines;
  • the cost of your CI/CD;
  • the security of your builds;
  • the access to specific resources (GPU, internal network).

In short: GitHub offers free, ready-to-use runners (hosted). If your needs are specific (GPU, private network, large volumes), you can also use your own machines (self-hosted).

What you will learn

  • Understand what a runner is and how a job is assigned to one
  • Tell apart GitHub-hosted and self-hosted runners, and their trade-offs
  • Choose the right kind based on performance, security and cost
  • Read the cost of a run per machine, and the macOS pricing trap
  • Route the jobs with runs-on labels

How does a runner work?

A runner is a server waiting for jobs to execute. Here is the simplified flow:

Execution flow of GitHub Actions runners

  1. You push code: an event triggers the workflow
  2. GitHub reads the YAML: it identifies the jobs and their runs-on labels
  3. The orchestrator assigns: each job goes to an available runner
  4. The runner executes: clones the repository, runs the steps, returns the results

The runs-on label is the key: it determines which kind of machine runs your job.

The two kinds of runners

GitHub-hosted runners: turnkey

These are virtual machines managed by GitHub. You install nothing, maintain nothing. They are ready to use.

jobs:
build:
runs-on: ubuntu-24.04 # A machine managed by GitHub

The main available labels:

SystemLabelsThe -latest alias
Ubuntu x64ubuntu-24.04, ubuntu-22.04ubuntu-latest points at 24.04
Ubuntu arm64ubuntu-24.04-arm, ubuntu-22.04-arm
Windows x64windows-2025, windows-2022windows-latest points at 2025
Windows arm64windows-11-arm
macOS arm64macos-26, macos-15, macos-14macos-latest points at macOS 26
macOS Intelmacos-26-intel, macos-15-intel

The specifications depend on the repository visibility, and that is the point people usually discover while comparing build times. On standard Linux and Windows x64 runners:

Repository visibilityCPURAMStorage
Public repository416 GB14 GB SSD
Private repository28 GB14 GB SSD

The same workflow therefore gets twice the power on a public repository. The macOS images have their own specifications, detailed in the GitHub reference linked at the end of this page.

The -latest aliases move under your feet

ubuntu-latest, windows-latest and macos-latest do not designate a version, but the version GitHub considers current, and that target moves. A workflow that ran on Windows 2022 yesterday can switch to Windows 2025 without a single line of the repository changing.

Pin an explicit version, ubuntu-24.04 rather than ubuntu-latest, for the same reason you pin an action by SHA: a reproducible build does not rest on a mobile reference.

What comes pre-installed: Node.js, Python, Java, Go, Ruby, Docker, git, curl, jq, AWS CLI, Azure CLI, kubectl, helm, and more. The full list lives at actions/runner-images.

Self-hosted runners: your machines

These are machines you manage (physical servers, VMs, containers). You install the GitHub Actions agent on them, and they become available to your workflows.

jobs:
train-model:
runs-on: [self-hosted, linux, gpu] # Your server with a GPU

Why choose self-hosted?

  • GPU or specialised hardware: hosted runners have no GPU
  • Private network: access to internal databases, private APIs
  • Large build volumes: savings on GitHub minutes
  • A custom environment: proprietary tools, specific licences
  • Long run times: no 6-hour limit as on hosted runners

A detailed comparison

CriterionGitHub-hostedSelf-hosted
MaintenanceNone (managed by GitHub)Yours
Start-up time20-40 secondsAbout 5 seconds (the machine is ready)
EnvironmentFresh on every jobPersistent (cache, tools)
Cost (public repos)Free and unlimitedYour infrastructure
Cost (private repos)Billed minutesFree (GitHub minutes)
Time limit6 h per jobUnlimited
Network accessInternet onlyInternal network possible
GPUNot availablePossible
SecurityFull isolationDepends on your setup

Self-hosted pricing: a story to follow

In December 2025, GitHub announced billing for self-hosted runners (around $0.002 per minute for the use of its orchestration platform), planned for 1 March 2026. Faced with the community reaction, GitHub suspended the measure. As of today, self-hosted runners remain free on the GitHub minutes side: you only pay for your own infrastructure. The subject is worth watching, since GitHub may revisit its position. See the GitHub changelog.

When do you use which?

Stay on GitHub-hosted if...

  • your builds are standard (Node.js, Python, Java, Go);
  • you need no private network access;
  • your jobs run for less than six hours;
  • you are on a public repository (free and unlimited);
  • you prefer zero maintenance.

Move to self-hosted if...

  • you need a GPU (machine learning, 3D rendering);
  • you must reach internal resources (a database, a private API);
  • your builds consume a lot of minutes on private repositories;
  • you have compliance constraints (sensitive data on premises);
  • you need a very specific environment.

Self-hosted and public repositories: be careful

A self-hosted runner on a public repository is a major security risk. Anyone can open a pull request that will execute code on your machine. Keep self-hosted runners for private repositories, or use protected environments.

The cost of GitHub-hosted runners

For public repositories: free and unlimited. That is one of the major advantages of open source on GitHub.

For private repositories, each plan includes a monthly quota, then usage is billed per minute, per machine. The old multiplier model, where a Windows minute consumed two and a macOS minute ten, has disappeared from the GitHub documentation in favour of a price list per runner type.

PlanIncluded minutes per month
GitHub Free2,000
GitHub Pro3,000
GitHub Team3,000
GitHub Enterprise Cloud50,000
Standard 2-core runnerPrice per minute
Linux x64$0.006
Linux arm64$0.005
Windows x64$0.010
macOS 3 or 4-core$0.062

The cost gap has not changed, only how you read it: a macOS minute is worth roughly ten Linux minutes, and a Windows minute about 1.7. A 10-minute macOS build therefore costs about what 100 minutes of Linux would.

Mind one consequence of the new model: the monthly quota only covers standard runners. As soon as you request a larger runner, more cores, a large ARM64 or a GPU, the run is billed from the first minute, even when your quota is untouched.

Optimising the cost

Use Linux whenever you can: do not run a Node.js build on macOS unless you specifically need to test on macOS.

jobs:
# ✅ Linux for the standard build
build:
runs-on: ubuntu-24.04
steps:
- run: npm ci && npm run build
# macOS only for the iOS and macOS specific tests
test-macos:
runs-on: macos-15
if: contains(github.event.pull_request.labels.*.name, 'test-macos')
steps:
- run: npm test

Move to self-hosted for large volumes: if you burn thousands of minutes a month, a dedicated server can work out cheaper.

Labels and job routing

The runs-on label decides which runner executes the job. It is a tag-matching system.

Labels of hosted runners

# The previous version, for a compatibility you must maintain
runs-on: ubuntu-22.04
# The explicit, current version: the default choice
runs-on: ubuntu-24.04
# Windows, explicit version
runs-on: windows-2025
# macOS on Apple Silicon
runs-on: macos-15
# macOS on Intel, if you still have to test on x86
runs-on: macos-15-intel

None of those lines uses -latest, and that is deliberate: an explicit version is the only one guaranteeing tomorrow's build runs on the same machine as today's.

Labels of self-hosted runners

When you register a self-hosted runner, you assign labels to it. GitHub routes jobs to the runners matching all the labels.

# A 64-bit Linux self-hosted runner
runs-on: [self-hosted, linux, x64]
# A runner with a CUDA 12 GPU
runs-on: [self-hosted, linux, gpu, cuda-12]
# A runner inside the production network
runs-on: [self-hosted, production-network]

Be specific with your labels

Avoid runs-on: self-hosted on its own. With several runners, the job could land on any of them. Use precise labels for predictable routing.

Good practices

1. Set timeouts

A stuck job can burn minutes for nothing (and block a self-hosted runner). Always set a reasonable timeout:

jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 30 # Kills the job after 30 minutes
steps:
- run: npm ci && npm run build

2. Use a matrix to test across several systems

Rather than duplicating jobs, use a matrix strategy:

permissions: {}
jobs:
test:
strategy:
matrix:
os: [ubuntu-24.04, windows-2025, macos-15]
runs-on: ${{ matrix.os }}
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: npm test

3. Secure your self-hosted runners

  • Isolation: one runner per project or per level of trust
  • Ephemeral: destroy and recreate the runners after every job
  • Updates: keep the GitHub Actions agent up to date
  • Monitoring: watch the activity and the resources

See Securing GitHub Actions for the details.

4. Make use of the cache

Hosted runners start from scratch on every job. Use the cache to speed your builds up:

- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}

Key points

  • Hosted runners are GitHub's machines, zero maintenance, ideal for 90 % of cases.
  • Self-hosted runners are your machines, for GPUs, a private network, or large volumes.
  • runs-on is the label routing the job to the right runner.
  • Public repositories get hosted runners free and unlimited.
  • Private repositories get a monthly quota then per-minute billing, a macOS minute costing about ten times a Linux one.
  • -latest is a mobile reference: pin ubuntu-24.04, never ubuntu-latest.
  • Self-hosted plus a public repository is a security risk, to be avoided.

External resources

For the operational details, the official documentation remains the reference.

Next steps

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn