# GitHub Actions runs-on Labels: Every Option

> Every runs-on label for GitHub-hosted runners with specs and rates, how multiple labels combine for self-hosted runners, and why a job can queue forever.

Source: https://latchkey.dev/learn/runner-reference/github-actions-runner-labels-reference  
Updated: 2026-08-20

Multiple labels are ANDed, not ORed. `runs-on: [self-hosted, linux, gpu]` needs one runner carrying all three labels, which is why jobs queue indefinitely instead of failing.

The `runs-on` key selects the machine, and two things about it surprise people: the specs differ between public and private repositories, and multiple labels require a single runner that has every one of them.

A job requesting a label combination no runner satisfies does not fail. It queues, forever, with no error.

## GitHub-hosted standard runners

| Label | Private repo | Public repo | Rate (private) |
| --- | --- | --- | --- |
| `ubuntu-latest` / `ubuntu-24.04` | 2 vCPU, 8 GB | 4 vCPU, 16 GB | $0.006/min |
| `ubuntu-24.04-arm` | 2 vCPU, 8 GB | 4 vCPU, 16 GB | $0.005/min |
| `windows-latest` | 2 vCPU, 8 GB | 4 vCPU, 16 GB | $0.010/min |
| `macos-latest` | 3 vCPU, 7 GB (arm64) | Same | $0.062/min |

> Standard runners are free and unlimited on public repositories, and public repos get the larger machine. The same workflow genuinely runs slower after a repository goes private.

## Larger runners

| Linux x64 size | Rate | arm64 rate |
| --- | --- | --- |
| 4-core | $0.012/min | $0.008/min |
| 8-core | $0.022/min | $0.014/min |
| 16-core | $0.042/min | $0.026/min |
| 32-core | $0.082/min | $0.050/min |
| 64-core | $0.162/min | $0.098/min |

> Larger runners are billed even on public repositories and even while included minutes remain. That is the most common billing surprise in this area.

## Labels are ANDed

```.github/workflows/ci.yml
# one runner must carry ALL of these labels
runs-on: [self-hosted, linux, x64, gpu]

# a single label
runs-on: ubuntu-latest

# a group plus a label (organisation runners)
runs-on:
  group: my-group
  labels: [self-hosted, linux]

# choose by matrix
strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
```

> If no online runner has every label, the job stays `queued` rather than failing. Check the labels on your runners against the labels on the job before looking anywhere else.

## Why a job queues forever

- No runner carries the full label set. Labels are ANDed, and this is by far the most common cause.
- The matching runner is offline or already busy at its concurrency limit.
- A typo in a label. Labels are matched literally, and there is no warning for one nobody has.
- A larger-runner label that is not configured for the repository or organisation.
- Concurrency limits for your plan are already saturated by other runs.

## FAQ

### How many vCPUs does ubuntu-latest have?

2 vCPU and 8 GB RAM on a private repository, 4 vCPU and 16 GB on a public one. The asymmetry is why the same workflow can feel slower after a repository is made private.

### Why is my job stuck in queued?

Almost always because no online runner carries every label the job requested. Multiple labels are ANDed, so `[self-hosted, linux, gpu]` needs one runner with all three, not three runners with one each.

### Are larger runners billed on public repositories?

Yes. Standard runners are free and unlimited on public repositories, but larger runners are always billed, including on public repos and while included minutes remain.

### How do I run a job on ARM?

Use an ARM label such as `ubuntu-24.04-arm`. ARM is cheaper on every provider that publishes both, typically 17% to 40% below the x64 rate for the same size.

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
