---
title: "High CI Queue Time in GitHub Actions | StarSling"
description: "Diagnose GitHub Actions runs that queue before a job starts: check concurrency groups, cancellation, runner capacity, and triggers in order."
url: https://starsling.dev/github-actions/problems/high-queue-time
canonicalUrl: https://starsling.dev/github-actions/problems/high-queue-time
---

# Why CI runs sit queued before any job starts

[GitHub Actions](https://starsling.dev/github-actions) / [Problems](https://starsling.dev/github-actions/problems) / Why CI runs sit queued before any job starts

- [How StarSling works](https://starsling.dev/)

Diagnosis mode: runtime. Last updated: 2026-08-31

Queued runs with no runner assigned almost always trace back to concurrency and cancellation settings that serialize or pile up work, not to a shortage of GitHub Actions capacity. Measure queue time from run history first, then confirm the cause before touching any workflow file.

## Table of contents

- [Symptoms](#symptoms)
- [How to diagnose it](#how-to-diagnose-it)
- [Likely causes](#likely-causes)
- [Hand it to an agent](#verify)
- [Related pages](#related-pages)
- [Related symptoms](#related-symptoms)
- [Sources](#sources)

<a id="symptoms"></a>

## Symptoms

When a job in a GitHub Actions run sits queued with no runner assigned, GitHub has accepted the run but has not yet started the job, and that delay is visible only in run history, not in the workflow YAML. The number to trust is a job's started_at minus its run's own created_at, pulled from the Actions REST API, because a gated job's own created_at is stamped only once its needs dependency resolves, which understates how long the run actually waited from trigger to first job start. Most repos hit this ceiling for one of a handful of reasons, and they compound: a concurrency group scoped too broadly serializes runs that could otherwise execute side by side, superseded pushes that were never cancelled keep occupying runner slots long after their result stopped mattering, a scarce runner label such as self-hosted, larger, or macOS creates its own queue independent of any concurrency setting, workflows without path filters multiply the number of runs competing for the same pool, and a handful of long-running jobs each hold a runner or a concurrency slot well past what their actual work requires. Checking them in this order moves from the most common, highest-leverage fix toward the narrower ones, because fixing concurrency and cancellation policy often clears most of the backlog before runner supply or trigger scope need any attention at all.

- A run sits in the Actions UI with a job marked Queued for well over a minute before any log output appears.
- Several runs for the same pull request or branch are all visible in the run list at once, and only the newest one is doing useful work.
- Queue time gets noticeably worse right after several commits land close together, even though nobody changed the workflow.
- One job in a run, often the one requesting a self-hosted or larger runner, queues far longer than the other jobs in the same run.
- A small change, like a README edit, produces the same long wait before its job starts as a full code change does.

<a id="how-to-diagnose-it"></a>

## How to diagnose it

[ci-speedup](https://starsling.dev/ci-speedup) carries the detection logic behind the causes below and opens the fix as a reviewable pull request. Install it with `npx skills add starslingdev/skills`, then run `/ci-speedup` in your repository.

To check by hand:

1. List recent runs with `GET /repos/{owner}/{repo}/actions/runs` (or `gh api repos/{owner}/{repo}/actions/runs`) and note each run's `created_at`.
2. For each run, pull its jobs with `GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs` and compute queue time per job as `job.started_at` minus the run's `created_at`, not the job's own `created_at`, which is stamped late for a gated job once its `needs:` dependency resolves.
3. Sort jobs by queue time and check whether the P90 exceeds roughly 60 seconds for pull request runs or 120 seconds for release or schedule runs.
4. For the worst offenders, group by `head_branch` and the `concurrency.group` key from the workflow file; check whether runs on the same branch overlap in time (cause: cancel-superseded), or whether unrelated jobs are queued behind one broad group (cause: concurrency-groups).
5. Read the queued jobs' `labels` field and compare against available runner capacity, using `GET /repos/{owner}/{repo}/actions/runners` for self-hosted pools, to rule in or out a scarce runner label (cause: right-sizing).
6. Check whether the queued runs were triggered by changes unrelated to the workflow's purpose, meaning it lacks `paths`/`paths-ignore` (cause: path-filter), and whether a job sharing that pool routinely runs long due to a slow setup or bloated post step (cause: long-running-jobs).

<a id="likely-causes"></a>

## Likely causes

Ordered by how often each one turns out to be the answer. Confirm a cause with its check before you change anything.

### 1. A concurrency group serializes runs that could execute side by side

When a concurrency group key is scoped too broadly, for example per-workflow instead of per-workflow-per-branch, every run sharing that key waits for the prior run in the group to finish before GitHub even assigns it a runner. The wait shows up as queued time in job history, but it is really the previous run's remaining duration, not a shortage of runners.

Confirm it: Pull the run's jobs with `GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs` and compare `started_at` against the run's `created_at` for runs sharing the same `concurrency.group` key defined in the workflow file; a wait that tracks the prior run's duration for that key points here.

Fix: [Concurrency groups](https://starsling.dev/best-practices/github-actions/cut-queue-time) (`ci.trigger.concurrency-groups`, detection mode runtime)

### 2. Superseded runs from the same branch pile up waiting for a runner

Without cancel-in-progress, every push to a branch keeps its prior run alive and running or queued, competing for the same limited runner pool even though nobody will use its result once a newer commit lands. A concurrency group with cancel-in-progress set to false, or no concurrency block at all on a push-triggered workflow, produces the same pileup.

Confirm it: List recent runs for the branch with `GET /repos/{owner}/{repo}/actions/runs?branch=<branch>` and check whether a later run's `created_at` falls before an earlier run's `updated_at`, meaning they overlapped; then check the workflow's `concurrency:` block for a missing group or `cancel-in-progress: false`.

Fix: [Superseded runs cancelled](https://starsling.dev/best-practices/github-actions/cancel-superseded-runs) (`ci.trigger.cancel-superseded`, detection mode hybrid)

### 3. Jobs are waiting on a scarce runner label, not a concurrency group

A job pinned to a runner label in limited supply, such as a larger hosted runner, macOS, or a self-hosted pool with only a few machines, queues behind every other job requesting that same label, even while standard Linux runners sit idle. This is a distinct mechanism from concurrency serialization: it is org-level or repo-level capacity for one label, not run-to-run ordering.

Confirm it: Read each queued job's `labels` field from `GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs`, and for self-hosted labels check `GET /repos/{owner}/{repo}/actions/runners` to see how many runners carry that label and whether they are already `busy: true`.

Fix: [Right-sized runners](https://starsling.dev/best-practices/github-actions/right-size-runners) (`ci.runner.right-sizing`, detection mode runtime)

### 4. A workflow without path filters runs, and queues, on every push

If a workflow's on block has no paths or paths-ignore, every commit, including a docs-only or unrelated-package change, triggers the same full workflow, multiplying the number of runs competing for the same runner pool and concurrency group. More runs in flight means a deeper queue for everyone sharing that pool, independent of any single run's own cost.

Confirm it: Inspect the heaviest workflow's trigger block with `yq '.on.push, .on.pull_request' .github/workflows/<file>.yml` for a missing `paths`/`paths-ignore`, then confirm with the run history whether a docs-only PR's commit produced a full run via `GET /repos/{owner}/{repo}/actions/runs?event=pull_request`.

Fix: [Path filters](https://starsling.dev/best-practices/github-actions/path-filter-workflows) (`ci.trigger.path-filter`, detection mode static)

### 5. A few long jobs occupy runners and concurrency slots longer than needed

A job with a slow, uncached setup step or a bloated post step, such as a large cache upload, holds its runner and its concurrency-group slot for longer than the actual build or test work requires. Every other run sharing that pool or group queues behind it for the full inflated duration, not just the useful portion of it.

Confirm it: Pull job timing with `GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs` for the runs sitting behind it, compute each step's duration from `steps[].started_at` and `completed_at`, and look for a setup step over 60 seconds or a Post step over 30 seconds inflating the job's occupied time.

Fix: [Long-running jobs split](https://starsling.dev/github-actions/optimizations/split-long-running-jobs) (`ci.parallel.long-running-jobs`, detection mode runtime)

<a id="verify"></a>

## Hand it to an agent

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to run this diagnosis against your repository and report which cause it found:

Diagnose high CI queue time in this repository using GitHub's run history, not the workflow file alone. Pull recent runs with `GET /repos/{owner}/{repo}/actions/runs` and their jobs with `GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs`. Compute each job's queue time as `job.started_at` minus the run's own `created_at`, not the job's own `created_at`, which is stamped late for a gated job once its `needs:` dependency resolves. Flag jobs whose queue time exceeds roughly 60 seconds for pull request runs or 120 seconds for release runs. Then test causes in this order: first, whether queued runs share an overly broad `concurrency:` group; second, whether runs on the same branch overlap in time, meaning superseded runs were never cancelled; third, whether queued jobs request a scarce runner label, checked against `GET /repos/{owner}/{repo}/actions/runners` for self-hosted pools; fourth, whether the workflow lacks `paths`/`paths-ignore` and runs on unrelated changes; fifth, whether a long-running job is occupying the shared pool or concurrency slot longer than its work requires. Report which cause the evidence supports before changing any workflow file.

<a id="related-pages"></a>

## Related pages

- [Cut CI queue time in GitHub Actions](https://starsling.dev/best-practices/github-actions/cut-queue-time)
- [Cancel superseded runs with concurrency and cancel-in-progress](https://starsling.dev/best-practices/github-actions/cancel-superseded-runs)
- [Right-size GitHub Actions runners without slowing CI](https://starsling.dev/best-practices/github-actions/right-size-runners)
- [Filter workflows with paths and paths-ignore in GitHub Actions](https://starsling.dev/best-practices/github-actions/path-filter-workflows)
- [Split long GitHub Actions jobs into parallel work](https://starsling.dev/github-actions/optimizations/split-long-running-jobs)
- [Why is GitHub Actions so slow](https://starsling.dev/github-actions-too-slow)

<a id="related-symptoms"></a>

## Related symptoms

Other symptoms on this site that share a likely cause with this one. If none of the causes above is yours, one of these is usually the page you wanted.

- [A GitHub Actions job stays queued or hangs with no output](https://starsling.dev/github-actions/problems/github-actions-jobs-stuck)
- [A job hits a timeout, or hangs until GitHub kills it](https://starsling.dev/github-actions/problems/github-actions-timeouts)
- [Why your test job is slower in CI than it is locally](https://starsling.dev/github-actions/problems/slow-tests)

<a id="sources"></a>

## Sources

- [REST API: List workflow runs for a repository](https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28)
- [REST API: List jobs for a workflow run](https://docs.github.com/en/rest/actions/workflow-jobs?apiVersion=2022-11-28)
- [Using concurrency to control workflow and job runs](https://docs.github.com/en/actions/using-jobs/using-concurrency)
- [Triggering a workflow: filtering by paths changed](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow#example-including-paths)
