skip to content

In GitHub Actions, what does runs-on select, and what is a hosted ubuntu-latest runner?

level: juniorimportance: must knowfreq 80%

answer

  1. It picks a machine, not a command
  2. Matching happens on labels
  3. A list of labels means all of them
  4. Fresh VM per job, nothing persists

basics

~10 s

runs-on picks the machine a GitHub Actions job executes on, by label. ubuntu-latest requests a GitHub-hosted Linux virtual machine that is created fresh for that one job and destroyed when the job finishes.

solid answer

~40 s

`runs-on` is a job-level key that selects a runner by **label**. Labels like `ubuntu-latest`, `windows-latest` and `macos-latest` map to GitHub-hosted runners; pinning `ubuntu-24.04` instead avoids a surprise when GitHub rolls `-latest` to a new image. A hosted runner is a clean virtual machine per job, with admin/sudo rights and a large pre-installed toolchain documented in the `actions/runner-images` repository. Nothing survives the job: installed packages, files and containers are discarded, which is why moving data between jobs needs `actions/upload-artifact` or `actions/cache`. You may give a list, `runs-on: [self-hosted, linux, x64]`, which requires a runner carrying **all** of those labels, not any one of them.

code

yaml · 11 lines
yaml
jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - run: ./gradlew build

  smoke:
    runs-on: [self-hosted, linux, x64]
    steps:
      - run: ./scripts/smoke.sh

go deeper

for a junior

Be ready to name the common hosted labels and say that each job gets a clean machine that is thrown away afterwards.

for a middle

Explain label matching as AND, why -latest can shift under you, and how artifacts or cache move data between jobs that cannot share disk.

for a senior

Show judgment about pinning images on release-critical workflows, reserving larger runners for jobs that actually parallelise, and diagnosing a stuck queue as a label mismatch.

for a principal

Own the fleet-level tradeoff: image pinning policy across hundreds of repositories, when the extra per-minute cost of larger runners is repaid by shorter wall-clock time, and how you migrate everyone off a deprecated image.

## The label-matching model Every job must declare where it runs. `runs-on` is that declaration, and it works by matching **labels**, never hostnames. GitHub's scheduler takes the labels in the job and dispatches the job to a runner that carries them all. ## GitHub-hosted runners `ubuntu-latest`, `windows-latest` and `macos-latest` are the standard GitHub-hosted labels. GitHub provisions, patches, and meters these machines. Three properties matter in interviews: - **Ephemerality.** Each job gets a fresh VM. There is no leakage of files, environment, or installed software between jobs or between workflow runs. This is what makes hosted runners safe to expose to pull requests from forks. - **Pre-installed tooling.** The images ship compilers, language runtimes, package managers, Docker (on Linux) and the CLI tools most builds expect. The exact inventory per image lives in the `actions/runner-images` repository, which is the honest answer to "is X installed?". - **Administrative access.** You get sudo (or administrator on Windows), so a job can install what the image lacks — at the cost of doing it again on every run, which is what `actions/cache` and container images exist to amortise. ## `-latest` versus a pinned image `ubuntu-latest` is an alias that GitHub re-points when a new LTS image is promoted. That rollover is announced but it *will* eventually change your toolchain underneath you. Pinning `ubuntu-24.04` or `macos-14` makes the change explicit: you bump the label when you are ready and you can bisect a breakage to the bump. Many teams pin on release-critical workflows and leave `-latest` on cheap ones. ## Lists mean AND ```yaml runs-on: [self-hosted, linux, x64] ``` This is not a preference order or a fallback list — the runner must carry every label. A frequent bug is listing two mutually exclusive labels, which produces a job that queues forever because no runner satisfies the set. GitHub eventually times a permanently queued job out rather than running it. ## Sizes and larger runners Standard hosted runners are modest machines. Organisations can configure **larger runners** with more vCPUs and memory, which are addressed by the custom label given at configuration time — so `runs-on:` still looks the same, only the label changes. Larger runners are billed at a higher per-minute rate, so they are a targeted tool for a genuinely parallel or memory-hungry job, not a blanket default. ## Consequence for workflow design Because each job gets its own machine, two jobs never share a filesystem. Anything one job produces that another needs must cross via artifacts, cache, or job outputs. Candidates who assume a shared working directory between `build` and `test` jobs are revealing they have only ever used a single-job workflow.

  • Two jobs in the same workflow both run on ubuntu-latest. Do they share a filesystem?
    No. Each job is scheduled onto its own runner and its own fresh VM, even when the labels are identical. Files produced by one job reach another only through `actions/upload-artifact` plus `actions/download-artifact`, through `actions/cache`, or as small string values via job outputs.
  • What happens if you put a label combination no runner has?
    The job queues rather than failing fast: GitHub keeps looking for a matching runner. If none appears, the job sits in the queue until GitHub's queue time limit expires and then fails. In practice this shows up as a permanently pending job after someone typos a self-hosted label or takes the only matching runner offline.

Think of it as ordering a rental laptop per task: you specify the spec sheet you need, it arrives wiped, and it is handed back and reimaged the moment you finish.

saying these in an interview costs you the question

  • Thinking runs-on names a specific host or IP
  • Assuming jobs share a working directory
  • Believing a label list means any-of
  • Expecting installed packages to persist to the next run
  • Treating ubuntu-latest as a frozen, never-changing image

context