GitHub Actions Per-Step timeout-minutes Not Cancelling a Hung Step
By Daniel Zoghalchali·Latchkey
A hung step runs far longer than expected because timeout-minutes was placed at the wrong level. A job-level timeout-minutes caps the whole job; a step-level one must be set on the individual step to cancel just that step.
What this error means
A single command hangs for the full job timeout (or the 6-hour ceiling) instead of being cancelled quickly, because no step-level timeout-minutes applied to it.
.github/workflows/ci.yml
jobs:test:runs-on:ubuntu-latesttimeout-minutes:30 # whole-job capsteps:- run:./flaky.sh # no per-step cap - can run the full 30m
Common causes
timeout-minutes at the job, not the step
A job-level timeout-minutes only cancels the entire job after the limit. To bound a single step, set timeout-minutes on that step.
No timeout at all on a hang-prone step
Without any timeout, a step blocked on a lock, prompt, or dead network waits until the job ceiling, wasting minutes.
How to fix it
Set timeout-minutes on the step
Add a tight per-step timeout so a hang fails that step in minutes.
.github/workflows/ci.yml
steps:- name:Integration testsrun:./integration.shtimeout-minutes:10 # cancels just this step
Layer step and job timeouts
Use step timeouts for individual hang-prone commands.
Keep a job timeout as a safety ceiling for the whole job.
Add non-interactive flags so steps do not block on input.
How to prevent it
Put timeout-minutes on hang-prone steps, not only on the job.
Keep a job-level timeout as an overall ceiling.
Run commands non-interactively so they never wait on a prompt.
Frequently asked questions
What causes "Per-step timeout"?
A job-level timeout-minutes only cancels the entire job after the limit. To bound a single step, set timeout-minutes on that step.
How do I fix Per-step timeout?
Add a tight per-step timeout so a hang fails that step in minutes.
Can Latchkey fix this automatically?
Yes. Latchkey runs your GitHub Actions on managed runners that detect this failure, apply the fix, and retry the job automatically - self-healing is on by default.