Introduction

Self-hosted runners in GitHub Actions provide dedicated compute resources for workflows that need specific environments, tools, or network access not available on GitHub-hosted runners. Organizations deploy self-hosted runners on their own infrastructure—in data centers, cloud VMs, or containerized environments. When configured correctly, GitHub dispatches workflow jobs to these runners based on label matching and runner group permissions.

When workflows remain stuck in the queued state with self-hosted runners, the root cause is almost always a mismatch between what the workflow requests and what GitHub can find available. The runner might be offline, its labels might not match the workflow's runs-on specification, the runner group might not permit the repository, or all matching runners might be busy with no capacity for new jobs. Understanding the runner lifecycle—from registration through online state to job execution—is essential for diagnosing queued workflow issues.

Self-hosted runners require continuous connectivity to GitHub's runner listener service. The runner application polls GitHub for job assignments, and if this connection breaks—due to network issues, service crashes, or host shutdown—the runner appears offline in GitHub's UI. Workflows targeting that runner remain queued indefinitely until a matching runner becomes available.

Symptoms

When GitHub Actions workflows are stuck in queued state due to self-hosted runner issues, you will observe these symptoms:

  • Workflow jobs show "Queued" status with no log output or start time
  • GitHub displays "Waiting for a runner to pick up this job" message
  • The runner appears as "Offline" or "Unavailable" in repository/organization settings
  • Workflows previously worked but now hang after infrastructure changes
  • Only certain workflows get stuck while others run successfully
  • The runner host is running but the runner service is stopped or crashed
  • Runner group visibility changes caused workflows to lose runner access

Common UI states indicating runner offline:

``` Job Status: Queued Started: - Runner: Waiting for self-hosted runner with labels: [self-hosted, linux, x64] Message: This job is waiting for a runner to pick it up.

# In runner settings Runner: my-runner-01 Status: Offline (Last seen: 2 days ago) Labels: self-hosted, linux, x64 ```

Runner application logs showing connection failure:

bash
[2026-01-15 10:30:00] Runner listener has stopped
[2026-01-15 10:30:01] Cannot connect to GitHub runner service
[2026-01-15 10:30:02] Error: Connection refused to https://github.com/_runner/
[2026-01-15 10:30:03] Runner is offline

Common Causes

Several factors cause self-hosted runner offline issues:

  1. 1.Runner service stopped or crashed: The runner application process stopped running due to manual intervention, system crash, or unhandled error. The host might be running, but the runner listener is inactive.
  2. 2.Runner host shutdown: The VM or machine hosting the runner was shut down, rebooted, or experienced hardware failure. The runner application can't connect when the host is off.
  3. 3.Network connectivity issues: Firewall rules, proxy configurations, or network outages prevent the runner from reaching GitHub's runner service endpoints. Corporate networks often block outbound connections.
  4. 4.Label mismatch: The workflow's runs-on specifies labels that don't match any registered runner. GitHub requires exact label matching for job assignment.
  5. 5.Runner group access restrictions: The runner is in a group that doesn't include the triggering repository. Runner group visibility settings control which repositories can use which runners.
  6. 6.Runner busy with no spare capacity: The runner is online and processing another job, and there are no additional runners to handle queued jobs. Single-runner setups commonly hit this bottleneck.
  7. 7.Runner registration token expired: During initial setup, if the registration token expired before the runner completed registration, the runner can't authenticate.
  8. 8.Runner application version incompatible: After GitHub updates the runner service API, older runner application versions may fail to connect properly.

Step-by-Step Fix

Follow these steps to diagnose and resolve self-hosted runner offline issues:

Step 1: Check runner status in GitHub

Verify the runner's online status:

```bash # Check via GitHub CLI gh api repos/:owner/:repo/actions/runners --jq '.runners[] | {name: .name, status: .status, labels: .labels[].name}'

# Check organization-level runners gh api orgs/:org/actions/runners --jq '.runners[] | {name, status, busy: .busy}'

# Or navigate in GitHub UI: # Repository > Settings > Actions > Runners # Organization > Settings > Actions > Runners > Runner groups ```

Expected output for healthy runner:

json
{
  "name": "my-runner-01",
  "status": "online",
  "labels": ["self-hosted", "linux", "x64"]
}

Offline runner output:

json
{
  "name": "my-runner-01",
  "status": "offline",
  "labels": ["self-hosted", "linux", "x64"]
}

Step 2: Check runner service on the host

Verify the runner application is running:

```bash # Check runner service status (systemd) sudo systemctl status actions.runner.*

# Check specific runner service sudo systemctl status actions.runner.owner-repo-runner-name

# If running as standalone process ps aux | grep Runner.Listener

# Check runner service logs sudo journalctl -u actions.runner.* -n 100 --no-pager ```

Expected service output:

bash
actions.runner.owner-repo.service - GitHub Actions Runner (owner-repo)
     Loaded: loaded (/etc/systemd/system/actions.runner.owner-repo.service; enabled)
     Active: active (running) since Mon 2026-01-15 10:00:00 UTC
   Main PID: 1234 (Runner.Listener)
      Tasks: 15
     Memory: 150M
     CGroup: /system.slice/actions.runner.owner-repo.service
             └─1234 /home/runner/run.sh Runner.Listener spawn

Step 3: Restart the runner service

If the runner is offline, restart it:

```bash # Restart via systemd sudo systemctl restart actions.runner.*

# Or restart specific runner sudo systemctl restart actions.runner.owner-repo-runner-name

# Enable auto-start on boot sudo systemctl enable actions.runner.owner-repo-runner-name

# If running manually cd /home/runner/actions-runner ./run.sh & ```

Step 4: Check runner logs for connection errors

Examine runner logs for specific failure reasons:

```bash # View runner log files cat /home/runner/actions-runner/_diag/Runner_*.log | tail -100

# Check for connection errors grep -E "error|fail|disconnect|offline" /home/runner/actions-runner/_diag/*.log | tail -20

# Watch logs in real-time tail -f /home/runner/actions-runner/_diag/Runner_$(date +%Y%m%d).log ```

Common error patterns:

bash
[ERROR] Cannot connect to GitHub runner service: Connection refused
[ERROR] Runner listener encountered an exception: TimeoutException
[ERROR] Failed to renew runner token: Unauthorized
[ERROR] Network request failed: Proxy authentication required

Step 5: Verify label matching

Confirm workflow labels match runner registration:

```bash # Get workflow runs-on specification grep "runs-on" .github/workflows/deploy.yml

# Expected workflow label format runs-on: [self-hosted, linux, x64]

# Check runner registered labels gh api repos/:owner/:repo/actions/runners --jq '.runners[].labels[].name' ```

Important label rules: - All specified labels must match (AND logic, not OR) - self-hosted is automatically added to all self-hosted runners - Labels are case-sensitive - Custom labels must be added during registration or via runner configuration

Add missing labels to runner:

```bash # On runner host, add custom label cd /home/runner/actions-runner ./config.sh --addlabels production,frontend

# Or reconfigure with new labels ./config.sh --url https://github.com/owner/repo --token REGISTRATION_TOKEN --labels self-hosted,linux,x64,production ```

Step 6: Check runner group permissions

Verify the repository can access the runner group:

```bash # Check runner group configuration gh api orgs/:org/actions/runners/groups --jq '.runner_groups[] | {id, name, visibility, repositories_url}'

# Check specific group details gh api orgs/:org/actions/runners/groups/:groupId --jq '{name, visibility, runners}'

# Or in GitHub UI: # Organization > Settings > Actions > Runners > Runner groups > [Group name] > Repository access ```

Runner group visibility options: - All repositories: Any repo in the organization can use the group - Selected repositories: Only specified repositories can use the group - Private repositories: Only private repos can use the group

If workflow repo isn't in allowed list:

bash
# Add repository to runner group access (via UI or API)
gh api -X PUT orgs/:org/actions/runners/groups/:groupId/repositories/:repoId

Step 7: Check network connectivity from runner host

Test connectivity to GitHub:

```bash # From runner host, test GitHub connectivity curl -v https://github.com/_runner/dispatch

# Check DNS resolution nslookup github.com dig github.com

# Test specific runner endpoints curl -v https://api.github.com/actions/runner/register

# Check proxy settings if required echo $HTTP_PROXY echo $HTTPS_PROXY

# Test through proxy curl -v --proxy http://proxy.company.com:8080 https://github.com ```

If network is blocked:

```bash # Configure runner to use proxy cd /home/runner/actions-runner ./config.sh --url https://github.com/owner/repo --token TOKEN --proxyurl http://proxy.company.com:8080 --proxyuser proxyuser --proxypass proxypass

# Or set environment variables export HTTP_PROXY=http://proxy.company.com:8080 export HTTPS_PROXY=http://proxy.company.com:8080 ./run.sh ```

Step 8: Add additional runners for capacity

If runners are consistently busy:

```bash # Check runner busy state gh api repos/:owner/:repo/actions/runners --jq '.runners[] | {name, busy}'

# If all runners show busy=true, add more runners # On new host: mkdir actions-runner && cd actions-runner curl -O https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-x64-2.321.0.tar.gz tar xzf actions-runner-linux-x64-2.321.0.tar.gz ./config.sh --url https://github.com/owner/repo --token REGISTRATION_TOKEN --labels self-hosted,linux,x64 sudo ./svc.sh install sudo ./svc.sh start ```

Verification

After fixing runner issues, verify workflows execute:

```bash # Trigger a test workflow gh workflow run test.yml --ref main

# Watch workflow status gh run watch

# Check runner picked up job gh run view --json jobs --jq '.jobs[] | {name, status, runner_name}'

# Expected output { "name": "build", "status": "in_progress", "runner_name": "my-runner-01" }

# Verify runner status is online gh api repos/:owner/:repo/actions/runners --jq '.runners[] | select(.name=="my-runner-01") | .status' # Expected: "online" ```

Check runner logs for successful job execution:

bash
tail -50 /home/runner/actions-runner/_diag/Runner_*.log | grep -E "Running job|Job completed"
# Expected:
# [INFO] Running job: build from workflow test.yml
# [INFO] Job completed with result: Succeeded

Prevention

To prevent self-hosted runner offline issues:

  1. 1.Run runners as systemd services: Use the runner's service installation for automatic restart and boot persistence.
bash
sudo ./svc.sh install
sudo ./svc.sh start
sudo systemctl enable actions.runner.*
  1. 1.Monitor runner health: Set up monitoring for runner service status and connectivity.
bash
# Simple monitoring script
#!/bin/bash
STATUS=$(systemctl is-active actions.runner.*)
if [ "$STATUS" != "active" ]; then
    echo "Runner service inactive" | mail -s "Runner Alert" admin@example.com
    systemctl restart actions.runner.*
fi
  1. 1.Standardize labels: Use consistent label conventions across all runners.
yaml
# Standard labels
runs-on: [self-hosted, linux, x64, production]
# Runner should have all four labels registered
  1. 1.Deploy multiple runners: For critical workflows, have at least 2 runners per label configuration to handle failures and load.
bash
# Scale runners horizontally
# Deploy runner-01, runner-02, runner-03 all with same labels
  1. 1.Configure runner auto-update: Enable automatic runner updates to stay compatible with GitHub API.
bash
# In runner config
./config.sh --url ... --token ... --autoupdate
  1. 1.Document runner dependencies: Maintain documentation of which workflows depend on which runners.
markdown
## Runner Inventory
- runner-01: [self-hosted, linux, x64, production] - workflows: deploy.yml, build.yml
- runner-02: [self-hosted, linux, x64, staging] - workflows: test.yml
  1. 1.Set up runner offline alerts: Configure GitHub Actions to notify when runners go offline.
yaml
# Monitor runner status workflow
name: Monitor Runners
on:
  schedule:
    - cron: '0 */1 * * *'  # Every hour
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: Check runner status
        run: |
          STATUS=$(gh api repos/:owner/:repo/actions/runners --jq '.runners[] | select(.status=="offline") | .name')
          if [ -n "$STATUS" ]; then
            echo "Offline runners: $STATUS"
            exit 1
          fi

Related Articles

  • [WordPress troubleshooting: Fix IAM Access Denied 403 - Complete Tro](fix-iam-access-denied-403)
  • [GitHub Actions Artifact Expired or 403 Download Failed](github-actions-artifact-expired-403-download-failed)
  • [Fix Github Actions Artifact Upload Failed File Too Large 5gb Limit Issue in GitHub Actions](github-actions-artifact-upload-failed-file-too-large-5gb-limit)
  • [Fix Github Actions Aws S3 Deploy Credentials Expired Rotate Issue in GitHub Actions](github-actions-aws-s3-deploy-credentials-expired-rotate)
  • [Fix Github Actions Cache Evicted Manually Workflow Fails Download Issue in GitHub Actions](github-actions-cache-evicted-manually-workflow-fails-download)

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "GitHub Actions Workflow Stuck in Queued Because the Self-Hosted Runner Is Offline", "description": "Resolve queued GitHub Actions workflows by checking self-hosted runner status, labels, runner groups, and service health on the runner host.", "url": "https://www.fixwikihub.com/github-actions-workflow-stuck-queued-self-hosted-runner-offline", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2026-01-23T03:57:09.026Z", "dateModified": "2026-01-23T03:57:09.026Z" } </script>