Runner Pool
A runner pool is the set of machines that accept GitHub Actions jobs for a given label, plus the rules that create and retire them. How depth sets queue time.
A runner pool is the set of machines that can accept a GitHub Actions job for a given label, together with the rules that decide when machines join the set and when they leave it. A queued job runs on whichever member of the matching pool is free, and it waits when every member is busy.
Nothing in workflow YAML names a pool. A job names labels through runs-on, and the pool is whatever set of online runners advertises those labels at the moment the job is queued.
Definition
Two properties describe a pool: membership and lifecycle policy.
Membership comes from labels and registration scope. GitHub routes a queued job by comparing the labels the job asked for against the labels each online runner advertises, and every label in the job must be present on the runner (workflow syntax reference, checked on 2026-08-13). Registration scope decides which repositories may queue onto the pool at all, since a runner is registered against a repository, an organization, or an enterprise. Runner groups apply a second filter on top of scope, restricting a set of runners to named repositories or workflows.
Lifecycle policy governs the size of the pool over time: how many machines exist while the queue is empty, how many may exist at peak, how quickly a new machine appears after a job queues, and how long a free machine survives before it is retired.
| Property | What it sets | Symptom when it is wrong |
|---|---|---|
| Label set | which jobs the pool can serve | jobs queue while machines in a neighboring pool sit idle |
| Scope | which repositories and workflows may queue onto it | a job waits with no logs and no assigned machine |
| Warm floor | machines kept running while the queue is empty | the first job after a quiet period waits on machine creation |
| Depth | ceiling on machines running at once | the queue grows through peak hours |
| Idle timeout | how long a free machine waits before retiring | either paid idle capacity or repeated cold starts |
Members are persistent or ephemeral. A persistent member stays registered and takes job after job, so caches, checkouts, and Docker layers from one job are on disk for the next, and state from one job leaks into the next. An ephemeral member starts the agent with --ephemeral, accepts exactly one job, then deregisters (self-hosted runner reference, checked on 2026-08-13). Ephemeral membership is what makes an autoscaled pool correct, because each machine has a defined end and the controller can create one machine per queued job.
Pools are static or autoscaled. A static pool holds a fixed number of machines that are registered once and left running, so depth and warm floor are the same number. An autoscaled pool watches the job queue and moves the member count between a floor and a ceiling, which a controller such as Actions Runner Controller does by reading queued jobs from GitHub and creating one runner for each.
Depth is the number that developers feel. A pool of four machines serves at most four jobs at a time, whatever the workflow file says about matrix width. Splitting one label into two also splits depth, because a static pool of eight machines divided into two pools of four can no longer put eight machines behind a single burst of matching jobs.
Example
This workflow fans out to twelve matrix legs, all asking for the same three labels:
name: integration
on:
push:
branches: [main]
jobs:
suite:
strategy:
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v4
- run: ./scripts/run-shard.sh ${{ matrix.shard }}Every leg is eligible for any runner advertising self-hosted, linux, and x64. If that pool holds four machines and each shard takes five minutes, the twelve legs run in three waves:
| Wave | Legs running | Legs still queued | Minutes after the push |
|---|---|---|---|
| 1 | 4 | 8 | 0 to 5 |
| 2 | 4 | 4 | 5 to 10 |
| 3 | 4 | 0 | 10 to 15 |
The last shard starts about ten minutes after the push, and the workflow reports about fifteen minutes of wall clock for five minutes of work. Nothing failed and no step was slow. The burst asked for twelve machines and the pool held four.
The queued legs show "Waiting for a runner to pick up this job" and produce no logs. That message reads the same whether the pool is full, the scope excludes the repository, or a label is misspelled, which is why the first check is whether the machines in the pool are actually busy. Idle machines alongside a waiting job point at membership rather than depth. GitHub cancels a job that has waited 24 hours for a self-hosted runner (GitHub Actions limits, checked on 2026-08-13), so a label mismatch surfaces as a cancellation a day after the push.
Raising depth to twelve collapses the three waves into one and returns the workflow to roughly five minutes. On a static pool that means twelve machines running all day for a burst that arrives on each push. On an autoscaled pool it means twelve machines created when the burst arrives and retired after the idle timeout, with each new machine paying its creation time before the first step runs.
Related Terms
- Autoscaling runners and how a controller sizes a fleet: the queue signals a controller reads, and the floor and ceiling it moves between.
- Queue time, the wait between a job being queued and starting: how the wait is measured and what separates a full pool from a cold start.
- Shortening GitHub Actions queue times: where the wait comes from in a real fleet and which lever moves it.
- GitHub workflow syntax reference for runs-on: the documented label matching behavior that decides pool membership.
- WarpBuild cloud runners documentation: the runner labels, sizes, and images available.
- Runners not picking up jobs: the scope, runner group, and workflow restriction checks to run when a job stays queued.
- WarpBuild pricing: per minute rates by runner type.
FAQ
What is a runner pool in GitHub Actions?
A runner pool is the set of machines that can accept a job carrying a given label, together with the policy that decides when machines are created and when they are retired. Workflow YAML never names a pool. A job names labels, and the pool is whatever set of online runners advertises those labels when the job is queued.
Is a runner pool the same as a runner group?
They answer different questions. A runner group is a GitHub access control object that decides which repositories and workflows may queue work onto a set of runners. A pool describes capacity: which machines exist, how many can exist at once, and how they are created and retired. One group can hold several pools, and one pool can be reachable through a group.
How many machines should a runner pool hold?
Size the ceiling against the widest burst you want served without waiting. Take the peak count of jobs that carry the pool's label at the same moment over a busy week, and set the maximum at least that high. Set the warm floor against the arrival pattern, since a floor of zero means the first job of the day pays for machine creation.
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.