The GitHub Actions timeout nobody sets (and the 6-hour job it hides)
A test deadlocks. A network call hangs waiting on a response that never comes. A watch process someone left in a script never exits. Without a timeout, none of these fail fast. The job just sits there holding a runner until GitHub finally kills it at the six-hour mark. Most repos never set a timeout, so the one day a job hangs, it hangs for as long as GitHub allows.
"No timeout" is not "no limit"
Leaving timeout-minutes off a job does not mean it can run forever, and it does not mean it runs unbounded until it finishes on its own. It means the job inherits GitHub's default ceiling of 360 minutes, six hours. A hung job burns minutes the whole way to that ceiling before it is cancelled. On a matrix that is six hours per stuck leg, and on a premium (macOS or Windows) runner those minutes bill at a multiple.
The fix
Set timeout-minutes on every job, sized a little above a normal run:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
...The number is a ceiling, not a target. If your build normally takes eight minutes, fifteen leaves headroom for a slow day while still killing a genuine hang in a fraction of the time the default would. A job that trips its timeout fails loudly, which is exactly what you want, instead of draining minutes in silence.
Job timeout vs step timeout
timeout-minutes on a job caps the whole job. You can also put it on a single step, which catches a hang closer to its source and stops one stuck step from eating the whole job's budget:
steps:
- name: Integration tests
run: ./run-integration.sh
timeout-minutes: 10Use a job-level timeout as the backstop on every job, and add a tighter step-level timeout on the one step most likely to hang, usually a network-bound or externally-dependent one.
Scheduled jobs are the worst case
A hung job on a pull request gets noticed, because a human is waiting on the check. A hung job on a schedule: cron has nobody watching. A nightly build with no timeout can ride the full six-hour ceiling every night and quietly repeat, and you find out from the bill, not from a red X. If you set a timeout on nothing else, set one on your scheduled workflows. The always-failing workflow guide covers the other way scheduled runs go bad.
Why it matters even on a public repo
On private repos and self-hosted runners a runaway job is a direct line on the bill. On a public repo the minutes are free, but a job stuck for hours still ties up a runner your real builds are queued behind, and it delays the moment CI tells you something is wrong. Either way, a timeout turns a silent six-hour stall into a fast, obvious failure. It is one line per job and almost nobody sets it.
Common questions
What is the default if I do not set it? 360 minutes, six hours, for a job on a GitHub-hosted runner. The job is cancelled at that point regardless of what it is doing.
What number should I use? A bit above a normal run for that job, not your worst case. The goal is to catch a hang, not to draw the line exactly at your longest healthy build. Ten to twenty minutes covers most test and lint jobs; size longer builds to match.
Does the timeout count queue time? No. timeout-minutes starts when the job begins executing, not while it waits for a runner, so a busy queue does not eat into it.
Can I set it higher than six hours? Not on GitHub-hosted runners. A hosted job is hard-capped at six hours of execution no matter what, so a timeout-minutes above 360 has no effect there. Self-hosted runners have no six-hour cap, which makes setting an explicit timeout on them matter even more.
Find out if you're missing it
It is one of the most common gaps, well-run projects included. honojs/hono, the framework GitSpider itself runs on, has a CI workflow where not one job sets timeout-minutes, and its scorecard flags it. See more real scorecards on the showcase, or pair this with the concurrency and why your CI is slow guides, then scan your own repo.
These hide across however many workflow files you have, which is exactly why nobody sits down and fixes them. Point GitSpider at your repo and it flags which patterns apply, with the fix for each.
Scan your repo free