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.
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
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
cd /path/to/actions-runner
tail -100 _diag/Runner_*.logLook 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.
# 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.comIf 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:
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 startStep 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.
ps aux | grep Runner.ListenerIf you don't see a Runner.Listener process despite the service reporting active, force a restart:
sudo ./svc.sh stop
sudo ./svc.sh start
sudo ./svc.sh statusIf this keeps happening repeatedly, check system resource exhaustion — runners on memory-constrained machines sometimes have their listener process OOM-killed silently.
dmesg | grep -i "oom\|killed process" | tail -20Step 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."
# .github/workflows/ci.yml
jobs:
build:
runs-on: [self-hosted, linux, x64, gpu] # ALL these labels must match exactly# Check what labels your runner actually registered with
cat .runner | grep -A5 labelsIf 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.
# Check ARC controller logs if running in Kubernetes
kubectl logs -n actions-runner-system -l app.kubernetes.io/name=actions-runner-controller --tail=100Error: 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
# 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.runner1And monitor it properly instead of finding out from a stuck PR:
- 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
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
GitHub Actions Artifact Upload Failing — Size Limit & Permissions Fix
Your GitHub Actions artifact upload is failing with 'upload artifact failed' or size limit errors. Here are the exact causes and fixes for the most common artifact upload failures.
GitHub Actions Cache Not Working — How to Fix It
Your workflow runs are still slow even with actions/cache. Cache miss every time, cache key conflicts, wrong paths — here's how to diagnose and fix GitHub Actions caching.
GitHub Actions Docker Push: Permission Denied / Unauthorized Fix (2026)
Getting 'permission denied' or 'unauthorized: authentication required' when pushing Docker images in GitHub Actions? Here are all the causes and fixes.