🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

GitHub Actions Self-Hosted Runner Shows Offline — How to Fix It

Your self-hosted runner shows 'Offline' in GitHub even though the service is running. Here's how to actually diagnose the cause — network, token expiry, or service crash — and fix it for good.

DevOpsBoysJun 17, 20264 min read
Share:Tweet

A self-hosted runner showing "Offline" in your repo's Actions settings, while you can see the process running on the machine, is one of the more confusing failure modes because the obvious check ("is it running?") says yes while GitHub says no.

Step 1: Check the Runner Service Status Directly

bash
sudo ./svc.sh status
/etc/systemd/system/actions.runner.myorg-myrepo.runner1.service
● actions.runner.myorg-myrepo.runner1.service - GitHub Actions Runner (myorg-myrepo.runner1)
   Loaded: loaded
   Active: active (running)

If this says active (running) but GitHub still shows Offline, the problem isn't the service — it's the connection between the service and GitHub. If it says failed or inactive, skip to the service crash section below.

Step 2: Check the Runner's Own Logs

bash
cd /path/to/actions-runner
tail -100 _diag/Runner_*.log

Look for these specific patterns:

Connection/network issue:

[ERROR] System.Net.Http.HttpRequestException: 
The SSL connection could not be established

This usually means outbound HTTPS to *.actions.githubusercontent.com is being blocked — check egress firewall rules or a corporate proxy that's intercepting/breaking TLS.

bash
# Test connectivity directly from the runner machine
curl -v https://api.github.com
curl -v https://github-releases.githubusercontent.com
curl -v https://pipelines.actions.githubusercontent.com

If any of these fail or hang, that's your root cause — not a GitHub-side problem.

Token expiry:

[ERROR] Http response code: Unauthorized from server pipelines.actions.githubusercontent.com

Self-hosted runner registration tokens used during initial setup expire, but the runner's own long-lived credentials (stored in .credentials after registration) shouldn't expire under normal operation. If you see Unauthorized on an already-registered runner, re-registration is usually the fastest fix:

bash
sudo ./svc.sh stop
./config.sh remove --token <new-removal-token-from-github-settings>
./config.sh --url https://github.com/myorg/myrepo --token <new-registration-token>
sudo ./svc.sh install
sudo ./svc.sh start

Step 3: Check for Silent Process Death (Service Says Running, But Isn't Really)

Sometimes the systemd unit reports "active" because the wrapper process is alive, but the actual runner listener process inside it has crashed and isn't restarting.

bash
ps aux | grep Runner.Listener

If you don't see a Runner.Listener process despite the service reporting active, force a restart:

bash
sudo ./svc.sh stop
sudo ./svc.sh start
sudo ./svc.sh status

If this keeps happening repeatedly, check system resource exhaustion — runners on memory-constrained machines sometimes have their listener process OOM-killed silently.

bash
dmesg | grep -i "oom\|killed process" | tail -20

Step 4: Check Runner Group / Label Mismatches (Looks Like Offline, Isn't)

Sometimes a runner is genuinely online but your workflow can't find it because the labels don't match, and teams mistake "no jobs assigned, ever" for "offline."

yaml
# .github/workflows/ci.yml
jobs:
  build:
    runs-on: [self-hosted, linux, x64, gpu]  # ALL these labels must match exactly
bash
# Check what labels your runner actually registered with
cat .runner | grep -A5 labels

If even one label in runs-on doesn't exist on any runner, the job queues forever with no error — it just sits "Waiting for a runner." This looks like an offline problem but is actually a label mismatch.

Step 5: Ephemeral Runners That Never Re-Register

If you're running ephemeral runners (one job per runner instance, common in Kubernetes-based runner setups via Actions Runner Controller), an "offline" runner showing up repeatedly often means the cleanup/registration cycle is broken.

bash
# Check ARC controller logs if running in Kubernetes
kubectl logs -n actions-runner-system -l app.kubernetes.io/name=actions-runner-controller --tail=100
Error: failed to register runner: POST https://api.github.com/...: 403 
Resource not accessible by integration

This specific error means your GitHub App or PAT used by the controller lacks the administration:write permission needed to register runners — a common gap when permissions were scoped narrowly during initial ARC setup.

Permanent Fixes Worth Setting Up

yaml
# A simple healthcheck cron that auto-restarts the runner service if 
# it's been silently dead for more than 5 minutes
*/5 * * * * systemctl is-active --quiet actions.runner.myorg-myrepo.runner1 || \
  systemctl restart actions.runner.myorg-myrepo.runner1

And monitor it properly instead of finding out from a stuck PR:

yaml
- alert: GitHubRunnerOffline
  expr: github_runner_status{status="offline"} == 1
  for: 5m
  labels:
    severity: warning

(Requires exporting runner status via GitHub's API into Prometheus — a simple scheduled script hitting GET /repos/{owner}/{repo}/actions/runners works fine for this.)

Set up self-hosted runners properly: How to Set Up GitHub Actions Self-Hosted Runner

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments