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-onlabels
How does a runner work?
A runner is a server waiting for jobs to execute. Here is the simplified flow:
- You push code: an event triggers the workflow
- GitHub reads the YAML: it identifies the jobs and their
runs-onlabels - The orchestrator assigns: each job goes to an available runner
- 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 GitHubThe main available labels:
| System | Labels | The -latest alias |
|---|---|---|
| Ubuntu x64 | ubuntu-24.04, ubuntu-22.04 | ubuntu-latest points at 24.04 |
| Ubuntu arm64 | ubuntu-24.04-arm, ubuntu-22.04-arm | |
| Windows x64 | windows-2025, windows-2022 | windows-latest points at 2025 |
| Windows arm64 | windows-11-arm | |
| macOS arm64 | macos-26, macos-15, macos-14 | macos-latest points at macOS 26 |
| macOS Intel | macos-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 visibility | CPU | RAM | Storage |
|---|---|---|---|
| Public repository | 4 | 16 GB | 14 GB SSD |
| Private repository | 2 | 8 GB | 14 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 GPUWhy 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
| Criterion | GitHub-hosted | Self-hosted |
|---|---|---|
| Maintenance | None (managed by GitHub) | Yours |
| Start-up time | 20-40 seconds | About 5 seconds (the machine is ready) |
| Environment | Fresh on every job | Persistent (cache, tools) |
| Cost (public repos) | Free and unlimited | Your infrastructure |
| Cost (private repos) | Billed minutes | Free (GitHub minutes) |
| Time limit | 6 h per job | Unlimited |
| Network access | Internet only | Internal network possible |
| GPU | Not available | Possible |
| Security | Full isolation | Depends 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.
| Plan | Included minutes per month |
|---|---|
| GitHub Free | 2,000 |
| GitHub Pro | 3,000 |
| GitHub Team | 3,000 |
| GitHub Enterprise Cloud | 50,000 |
| Standard 2-core runner | Price 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 testMove 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 maintainruns-on: ubuntu-22.04
# The explicit, current version: the default choiceruns-on: ubuntu-24.04
# Windows, explicit versionruns-on: windows-2025
# macOS on Apple Siliconruns-on: macos-15
# macOS on Intel, if you still have to test on x86runs-on: macos-15-intelNone 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 runnerruns-on: [self-hosted, linux, x64]
# A runner with a CUDA 12 GPUruns-on: [self-hosted, linux, gpu, cuda-12]
# A runner inside the production networkruns-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 build2. 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 test3. 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-onis 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.
-latestis a mobile reference: pinubuntu-24.04, neverubuntu-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.
- GitHub-hosted runners: official documentation
- Self-hosted runners: the installation guide
- actions/runner-images: the software pre-installed on hosted runners
Next steps
- Securing the runners: hardening a self-hosted runner, to be done before the first production use.
- Ephemeral runners: the disposable pattern removing any persistence between two jobs.
- Runner maintenance: keeping a fleet up to date, monitored and sized for the real load.