Skip to content

Blog

GitHub Actions "Waiting for a runner to pick up this job": causes and fixes

·

A job that sits on Waiting for a runner to pick up this job has been accepted by GitHub and handed to nobody. The page does not say why, and the same words cover several different causes. This post is the list of reasons, in the order worth checking them, with what each one looks like and how to fix it. The GitHub facts in it were read from GitHub’s own documentation on 10 November 2026; where something is ours, it says so.

First, which kind of waiting is it?

There are two different waits behind the same words. In the first, a runner exists that could take the job and is busy or not yet started: the wait ends by itself. In the second, no runner could ever take the job, because nothing matches it: the wait ends only when GitHub gives up. On our runners the second is by far the more common of the two, and it is the one that looks like an outage and is not. So check the label before you check the capacity.

Cause 1: the label matches no runner

A job goes to a runner that has every label in its runs-on. GitHub’s documentation puts it plainly: the labels operate cumulatively, so a runner missing any one of them is not eligible. That has three everyday consequences. A typo in one label leaves the job with no eligible runner. Labels spread across two runners match neither. And a runner created without its default labels will not match a job that asks for self-hosted, an operating system or an architecture.

jobs: build: # waits: no runner carries this label runs-on: runners-ubuntu-2404 build-fixed: runs-on: runnersio-2vcpu-ubuntu-2404

On our runners the same mistake has two shapes. A label that does not start with runnersio- is not addressed to us at all, so the job never reaches our systems and appears nowhere on our side; it waits until GitHub times it out. A label that does start with runnersio- but is not one we sell is refused, and the job’s page names the label to change. The list of labels is the thing to copy from, and moving from GitHub-hosted runners says what to put where ubuntu-latest used to be. The ARM and Windows labels have their own suffixes, described on the ARM64 and Windows pages.

Cause 2: the runner exists but is not there

With self-hosted runners, the label can be right and the machine gone: stopped, out of disk, or deregistered. GitHub’s limits page says a job can be in the queue for 24 hours before it is automatically cancelled, so a dead runner shows as a long wait and then a cancelled run. In the repository, Actions, then Runners in the left sidebar, lists the runners and their labels, so you can compare them with the runs-on value. If the runner is offline, nothing on the workflow side can help.

GitHub-hosted runners have a bound of their own. GitHub’s description of workflow continuity says that a workflow run that was queued but not processed by a GitHub-hosted runner within 45 minutes is discarded. Both bounds are GitHub’s, and neither tells you why the job was not picked up, so a wait that long is a reason to work down this list rather than to wait longer.

Cause 3: you are at your concurrency cap

Every plan caps how many jobs run at once. GitHub’s limits page lists 20 concurrent jobs on Free, 40 on Pro, 60 on Team and 500 on Enterprise for standard GitHub-hosted runners, and 1,000 for larger runners on Team and Enterprise. A matrix of a hundred jobs on a Team plan cannot all run at once: sixty can, and the rest have to wait for a slot. The fix is a smaller fan-out or a higher cap, and GitHub’s page says its Support can raise the limits on a support ticket.

Fewer, shorter runs also queue less. The post on running less has the concurrency groups, path filters and timeouts that cut the number of jobs asking for a slot in the first place, and the pricing post covers what waiting and running each cost on GitHub’s side.

Cause 4: the runner group does not include this repository

Larger runners are reached through runner groups, and a group controls which repositories may use it. GitHub’s documentation says that by default repositories in an organisation do not have access to enterprise-level groups, that organisation-level groups are open to all repositories unless an owner restricts them, and that you must configure the group’s permissions before workflows can send jobs to larger runners. A repository outside the group has no runner it may use, so its job cannot be placed. GitHub’s page does not say what such a job shows, so do not rely on a message. Look at the group’s repository access list before you look at anything else if only some repositories are stuck.

Cause 5: a concurrency group is holding the run

A concurrency: key in the workflow allows one running run per group. A second run in the same group waits as pending until the first finishes, and by default a newer pending run replaces an older one. That is a wait you asked for, and it is worth ruling out when the same workflow is the only one that stalls.

Cause 6: GitHub itself is slow

Rarely, nothing on your side is wrong. If every workflow in every repository is waiting at once, and nothing changed, look at GitHub’s status page before you change anything.

A checklist, in order

Compare the label against the list of labels you can actually use, character by character. Then check the runner is online, or that the repository is in the runner group. Then count the jobs running in the organisation against your cap. Then look for a concurrency group, and last, the status page. Most waits end at the first step.

How runners.io makes the wait say why

The thing that makes this hard on GitHub is that the page says the same words for every cause. On our runners a job that is waiting, or that we stopped, says why at the top of its own page, in plain words: Waiting — you’re already running as many jobs as your plan allows, Paused — there’s no payment method on your account, or that the job was refused and which label to change. The troubleshooting page lists each message and its fix, and the jobs page explains the states a job moves through.

Waiting costs nothing. Time is counted from when your work starts running to when it stops, and waiting for a machine is not billed. A job over your concurrency limit waits until one of your other jobs finishes, and an owner or an admin can set a lower limit than the plan allows, to bound what the organisation spends at once. Your plan’s figure and the rules are on the limits page.

Two honest limits. Capacity is finite, so a job can still wait for a free machine, and the page says so rather than leaving you to guess. And a wrong label is still a wrong label: a prefix that is not ours never reaches us, which is why the first thing the troubleshooting page asks about is the label. When a queue matters more than the cost of jumping it, the -priority suffix on a label places a job ahead of your other queued jobs, at a higher rate per minute that the labels page states; it never stops a job that is already running.

If a wait is ours rather than yours, it shows on the status page: the runners read Degraded when a job we could run has waited more than ten minutes, and Outage past thirty. A wait caused by your own concurrency limit, plan, card or a repository our App cannot see does not count there, because that wait is shown on the job’s own page.

If you are moving over

Most label mistakes happen during a move, when forty workflow files each need a new runs-on. On the migration page, connect your GitHub organisation and we open the pull request for you, and its own checks run on our runners, so you see the labels work before you merge. The pricing page has what a minute costs on each size.

Run your CI and your infrastructure on one bill

Connect GitHub

Card required. You pay only for the minutes your jobs run.