# GitHub Actions runner offline

> Fix a GitHub Actions runner offline: the three states GitHub means by Offline, how to tell them apart, and how to stop the runner dropping out again.

Source: https://latchkey.dev/learn/failures/self-hosted-runner-offline  
Updated: 2026-09-20

A GitHub Actions runner offline in your settings means GitHub is not hearing from it, and the documentation gives three reasons for that exact status: the machine is off, the runner application is not running, or it cannot communicate with GitHub. Jobs aimed at its labels queue rather than fail, so the symptom you notice first is usually a pull request that never starts.

## What this error means

The Runners page under repository or organization settings lists the machine with a status of Offline, and every job whose `runs-on` matches its labels sits in the queue. GitHub's own definition of the status is the diagnosis worth reading first: "The runner is not connected to GitHub. This could be because the machine is offline, the self-hosted runner application is not running on the machine, or the self-hosted runner application cannot communicate with GitHub." Nothing fails, so no notification fires; a job queued for a self-hosted runner is cancelled after 24 hours, which is often the first alert anyone gets.

```Settings > Actions > Runners (GitHub's own wording for the status)
Status: Offline
The runner is not connected to GitHub. This could be because the machine is offline, the
self-hosted runner application is not running on the machine, or the self-hosted runner
application cannot communicate with GitHub.
```

## Common causes

### The runner application is not running after a reboot

Started interactively with `run.sh` in a shell session, or installed as a service that was never enabled at boot. The host is up, the network is fine, and nothing is listening for jobs. This is the most common cause and the easiest to prevent permanently.

### You restarted the service and it went Offline again

This is the loop that wastes the afternoon. If the agent comes back and drops out within minutes, it is being killed rather than failing to start: memory pressure on the host with the service manager restarting it, a registration token or credential that no longer authenticates, or a clock far enough out of sync that TLS is refused. Read the runner's own diagnostic log instead of restarting a third time; it names which of the three it is.

### The machine is off, stopped or reclaimed

A spot or preemptible instance taken back, a VM stopped by a cost policy, a laptop somebody closed, a node drained by a cluster autoscaler. The status is accurate and the fix is capacity rather than configuration.

### The host cannot reach GitHub

A proxy that the service unit does not know about, a firewall change, a TLS-intercepting appliance with an expired certificate, or DNS that resolves inside your network and not outside it. The configuration script's check tests every service the agent needs and is the fastest way to confirm or rule this out.

## How to fix it

### Install the runner as a service and enable it at boot

1. Stop any interactive `run.sh` session on the host.
2. Install the service with the bundled script, which registers it with the host's service manager.
3. Start it, confirm the status, and reboot once to prove it comes back on its own.

```Terminal, on the runner host
cd ~/actions-runner
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status
```

### Run the connectivity check as the service account

Environment matters more than the result here: a check that passes in your interactive shell and fails under the service account has found the problem. Point the check at the same URL the runner was configured with, and give the service unit the proxy variables if your network needs them.

```Terminal, on the runner host
./config.sh --check --url https://github.com/ORG/REPO --pat $GH_PAT

# proxies belong in the unit environment, not only in your shell
sudo systemctl show actions.runner.* -p Environment
```

### Read the diagnostic log when it will not stay connected

The agent writes its own log on the host, and it is more forthcoming than the settings page. The last lines before each disconnect distinguish a process that was killed from one that failed to authenticate from one that could not open a connection at all.

```Terminal, on the runner host
tail -100 "$(ls -t ~/actions-runner/_diag/Runner_*.log | head -1)"
journalctl -u "actions.runner.*" --since "-2 hours" | tail -50
timedatectl status | grep -i synchronized
```

### Re-register the runner when credentials are the problem

Remove it cleanly and configure it again with a fresh token rather than editing the credential files. A clean re-registration takes a minute and rules out a whole class of half-repaired state, and it is the right move after any host rebuild.

```Terminal, on the runner host
./config.sh remove --token <removal-token>
./config.sh --url https://github.com/ORG/REPO --token <registration-token> --labels linux,x64,gpu
```

### Alert on the status, not on the failure

Nothing fails when a runner goes offline, so nothing tells you. Poll the runners API on a schedule and raise an alert on any runner that is offline for longer than a few minutes, which turns a queued pull request nobody understands into a page that names the machine.

```Terminal
gh api /repos/{owner}/{repo}/actions/runners \
  --jq '.runners[] | select(.status != "online") | "\(.name) is \(.status)"'
```

## How to prevent it

- Install the runner as a service and verify it survives a reboot, on every host, once.
- Alert on offline runners directly; a queued job is a lagging indicator and a 24 hour cancellation is a worse one.
- Keep a time daemon running on every runner host, because clock drift fails TLS and nothing else explains it.
- Give hosts headroom so the agent is not killed under memory pressure and restarted in a loop.
- Re-register cleanly after any host rebuild instead of restoring credential files.

## Three questions, in this order

Can you reach the host at all? If not, the machine is the answer and nothing else on this page applies. Power, network, or a cloud instance that was stopped or reclaimed.

Is the runner process running on it? A reboot without the runner installed as a service is the single most common cause of a runner that was fine yesterday. Check the service, not a shell you left open in a terminal multiplexer three weeks ago.

Can the process reach GitHub? The runner ships a check for exactly this, and it tests every network service the agent needs rather than just whether a browser can load github.com. Run it as the account the service runs as, because a proxy variable in your shell is not a proxy variable in the unit file.

```Terminal, on the runner host
sudo ./svc.sh status
./config.sh --check --url https://github.com/ORG/REPO --pat $GH_PAT
```

## When it comes back and then drops out again

A runner that reconnects and goes Offline within minutes is not the same problem as one that has been down since a reboot. The usual causes are a host-level resource problem, where the agent is killed under memory pressure and restarted by its service manager in a loop, and credentials, where the registration has been invalidated and the agent cannot re-authenticate.

Clock drift belongs on this list too, because it produces a failure that looks like nothing else: a host whose time is far enough out will have its TLS handshakes rejected, the agent will fail to connect, and every other check will pass. Install a time daemon and confirm it is running before spending an afternoon on certificates.

The other quiet one is scope. A runner removed from the repository or organization, or one whose runner group lost access, is not offline so much as unwanted, and it will keep showing a status while never being offered work.

## There is no Latchkey runner to go offline

This page has no reproduction and no detection pattern, because the failure is not something that happens during a job: it is the absence of a machine. Latchkey has no pattern for it either, and the reason is structural. A Latchkey runner is created for one job and destroyed when that job ends, so there is no long-lived agent holding a connection open between runs, no service to survive a reboot, and no status column that can go stale.

The provisioning path is [the documented one](/documentation/runner-provisioning): a job labeled for a Latchkey runner is handed to warm capacity in seconds, or a fresh machine is launched in about ten seconds. Nobody patches it, nobody restarts it, and it does not exist between jobs.

The trade is real. You give up the machine: no SSH into a box you control, no cache warmed on local disk between jobs. And queueing does not go away; it has its own causes anywhere. What you stop maintaining is a fleet your pull requests depend on.

## FAQ

### Why does my self-hosted runner keep going offline?

A runner that reconnects and drops out repeatedly is being killed rather than failing to start. The three usual causes are memory pressure on the host with the service manager restarting the agent, credentials that no longer authenticate, and a system clock far enough out of sync that TLS handshakes are refused. The runner's diagnostic log on the host names which one it is.

### How do I restart a GitHub Actions self-hosted runner?

Use the bundled service script on the host: `sudo ./svc.sh stop` then `sudo ./svc.sh start`, and confirm with `sudo ./svc.sh status`. If it was started interactively with `run.sh`, install it as a service instead, because an interactive session will not survive the next reboot or the next closed terminal.

### What happens to the jobs that were queued for it?

They wait. A job queued for a self-hosted runner is automatically cancelled after 24 hours, and until then it sits with no annotation and no failure.

### Should I run the runner as a service or with run.sh?

As a service, on any machine that other people depend on. `run.sh` is for trying it out: it dies with the terminal, it does not come back after a reboot, and it produces exactly the failure this page is about. The service installer ships with the runner and takes one command.

## References

- [GitHub Docs: monitor and troubleshoot self-hosted runners, including the Offline status](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/monitor-and-troubleshoot)
- [GitHub Actions limits: the 24 hour queue cancellation for self-hosted jobs](https://docs.github.com/en/actions/reference/limits)
- [actions/runner#2662: the runner has received a shutdown signal](https://github.com/actions/runner/issues/2662)
- [Latchkey documentation: ephemeral runners, one job per machine](/documentation/runners-overview)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
