ACTIONS RUNNER STARVATION CHECK · RUNS IN YOUR BROWSER

Is your red GitHub Actions check even real?

A job GitHub never assigns a runner to fails in a few seconds with an empty output — indistinguishable at a glance from a real, fast failure. Paste the run's own JSON below and this page reads the one field that actually decides: runner_id. Zero means no runner was ever scheduled, so no step ever ran — the failure is not your code.

Nothing is uploaded. The JSON you paste is read in this browser and never leaves your device.

1. Get the run's job JSON

Run this for the failing run (needs the gh CLI, already logged in):

gh api repos/OWNER/REPO/actions/runs/RUN_ID/jobs

No gh CLI? Open https://api.github.com/repos/OWNER/REPO/actions/runs/RUN_ID/jobs in a browser tab authenticated with a token that has Actions: read, and copy the response body.

2. Paste it here

Paste the JSON from step 1 and click Check.

Prefer a CLI, or want this in a workflow step?

Same logic, zero dependencies, one file:

python3 check.py --run RUN_ID          # exits 1 if any job was starved
python3 check.py --run RUN_ID --json   # machine-readable
python3 check.py --from-json jobs.json # classify a payload you already have, no API call

Get check.py from the repo — stdlib only, nothing to install. It talks to the API itself via the gh CLI or a GH_TOKEN/GITHUB_TOKEN it finds in your environment, which is the route a hosted CI step or an agent session actually has.

Common questions

Why did my check fail in 3 seconds with no output?

That is the signature of a job GitHub never assigned a runner to — included minutes spent, a spending limit, a payment problem, or Actions disabled on the account. Every check fails within a few seconds with an empty body, and it looks identical to a real fast failure from the checks list alone.

Why not just go by how fast it failed?

Because a real job can fail in three seconds too — a syntax error or a failing assertion on the first test does not need a runner-minute to fail. Duration looks the same either way; only runner_id tells them apart, which is the one field this tool decides on.

Does this tell me why my account stopped getting runners?

No — only whether a runner was assigned. GitHub's own run-timing endpoint reports zero billable time for private repos regardless of what ran, so it cannot attribute cost either. That answer is on your GitHub billing page; this tool only tells you whether the next few minutes are worth spending on your code or on your Actions usage.

Why do I have to paste JSON instead of it just checking for me?

Checking for you would mean either a server that sees every token pasted into it, or this page fetching on your behalf with a token it would have to hold. Neither is worth the convenience next to pasting the response you already fetched yourself with your own credentials.

More: why a GitHub Actions check goes red in seconds with no output, add a starvation guard step to your own workflow so a starved run reports itself, or this checker vs. just re-running the job.

Related: if the job did get a runner and tests flip between runs, the flaky test finder classifies which tests are flaky versus broken from JUnit XML you paste — nothing uploaded.

Related: if the job did get a runner and line coverage fell on the PR, the coverage drop check compares two Cobertura or JaCoCo reports and names the files that got worse — nothing uploaded.