DevOpsil
CI/CD
85%
Needs Review

Fix GitHub Actions Workflow Stuck in 'Queued'

Riku TanakaRiku Tanaka3 min read

The Symptom

You push a commit or trigger a workflow, and the GitHub Actions run shows "Queued" indefinitely. The job never picks up a runner and the status spinner keeps spinning with no progress.

Queued — Waiting for a runner to pick up this job...

Root Cause

A workflow stays queued when no runner is available to execute it. The most common causes are:

  1. Self-hosted runner is offline. The runner machine is stopped, the runner service crashed, or the runner lost connectivity.
  2. Label mismatch. The job requests runs-on: custom-label but no registered runner has that label.
  3. Concurrency limits hit. Your plan's concurrent job limit is reached, or a concurrency group is blocking the run.
  4. GitHub-hosted runner capacity. Rare, but GitHub occasionally has capacity issues for specific runner types (especially macOS or large runners).
  5. Required approvals. Workflows from fork PRs or first-time contributors require manual approval.

Step-by-Step Fix

1. Check if approval is required

Go to the Actions tab in your repository. If you see a yellow banner saying "Approve and run," click it to approve the workflow. This is common for first-time contributors.

2. Verify self-hosted runner status

Go to Settings > Actions > Runners and check the runner status:

# On the runner machine, check the service
sudo systemctl status actions.runner.*.service

# If stopped, start it
sudo systemctl start actions.runner.*.service

# Check runner logs
cat /home/runner/actions-runner/_diag/Runner_*.log | tail -50

3. Verify runner labels match

Compare the runs-on value in your workflow with the runner's assigned labels:

# Workflow file
jobs:
  build:
    runs-on: [self-hosted, linux, x64, production]

The runner must have ALL listed labels. Check in the repository settings or via the API:

gh api repos/{owner}/{repo}/actions/runners --jq '.runners[] | {name, status, labels: [.labels[].name]}'

Fix a label mismatch by updating either the workflow or the runner's labels in the settings UI.

4. Check concurrency groups

If your workflow uses concurrency groups, a pending run may be waiting for a previous run to complete:

concurrency:
  group: deploy-production
  cancel-in-progress: false

With cancel-in-progress: false, new runs queue behind the active one. Check the Actions tab for an in-progress run in the same group. Either wait for it to finish or cancel it:

gh run list --workflow=deploy.yml --status=in_progress
gh run cancel <run-id>

5. Check your plan's concurrency limits

GitHub plans have concurrent job limits:

PlanConcurrent jobs
Free20
Team40
Enterprise500

For self-hosted runners, there is no GitHub-imposed limit, but your runner pool size is the practical limit.

Check how many jobs are currently running:

gh run list --status=in_progress --limit=50

6. Re-run the workflow

If the runner is healthy but the job is still stuck (possibly a transient GitHub issue):

gh run rerun <run-id>

Prevention Tips

  • Monitor self-hosted runners. Set up health checks and alerts for the runner service. Use a process manager like systemd to auto-restart the service on failure.
  • Use runner scale sets. For Kubernetes-based runners, use Actions Runner Controller (ARC) to auto-scale runners based on demand.
  • Set cancel-in-progress: true for non-critical workflows. This prevents queue pileups on frequently triggered workflows like CI checks.
  • Add a timeout to jobs. Use timeout-minutes to prevent zombie runs from blocking concurrency groups indefinitely.
  • Use GitHub's larger runners for predictable capacity. GitHub-managed larger runners provide dedicated capacity and avoid shared pool contention.
Share:

Was this article helpful?

Riku Tanaka
Riku Tanaka

SRE & Observability Engineer

If it's not measured, it doesn't exist. SLO-driven, metrics-obsessed, and the person who gets paged at 3 AM so you don't have to. Observability isn't optional.

Related Articles

More in CI/CD

View all →

Discussion