Best practice · Runner & Queue

Bound GitHub Actions runner jobs with timeout-minutes

Set `timeout-minutes` on every GitHub Actions job that executes on a runner, plus hang-prone steps, so a stuck run is cancelled in minutes instead of occupying a runner for up to the 360-minute default.

AI agents open the PR

StarSling agents inspect your workflow, apply this optimization, and open a reviewable PR automatically.

Do this: bound the job and hang-prone steps

Every job that executes on a runner declares a timeout-minutes value above its normal runtime, with enough headroom for healthy long-tail runs. A reusable-workflow caller job that uses jobs.<job_id>.uses cannot accept this key; audit the executable jobs inside a repository-local called workflow instead. Steps that call flaky networks, integration services, or test processes that can hang may also carry a tighter step-level timeout. The implicit 360-minute job default is never the operating limit.12

A bounded job with a tighter test-step timeout
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 20          # whole-job bound: normal runtime plus headroom
    steps:
      - uses: actions/checkout@v7
      - run: npm ci
      - run: npm test
        timeout-minutes: 10      # tighter bound on the hang-prone step

Avoid this: rely on the 360-minute default

A hung job keeps a runner busy for up to six hours before GitHub cancels it, and every rerun can repeat the waste.

An unbounded job that can run to GitHub's default
jobs:
  test:
    runs-on: ubuntu-latest
    # No timeout-minutes. A hung step can keep this job and its runner
    # busy for up to the 360-minute default before GitHub cancels it.
    steps:
      - uses: actions/checkout@v7
      - run: npm ci
      - run: npm test

Shipped by StarSling

On Mastra, StarSling bounded every StarSling-hosted job with `timeout-minutes` so a hung job is killed fast instead of running to the 360-minute default.

Job-level vs step-level timeout-minutes

A job-level timeout-minutes caps the whole job, including every setup and teardown step. A step-level timeout caps only that step. Use the job bound as the required safety net, then add a tighter step bound where a test process or network call can hang. Size both from observed healthy runtimes plus headroom so normal tail latency does not become a false failure.

Why it matters

Without an explicit bound, a stuck job can occupy runner capacity for GitHub's 360-minute default and delay every developer waiting behind it. A right-sized timeout releases the runner, produces a faster red signal, and limits the cost of each hang without changing healthy runs.

When to set timeout-minutes

Use it when

Every runner-executing job should have a bound. Start from its observed healthy runtime, include the long tail, and add deliberate headroom rather than choosing one value for every workflow. Reusable-workflow caller jobs cannot carry this key; bound the jobs inside a local called workflow instead.

Be careful when

Do not leave legitimately long builds, end-to-end suites, or release jobs unbounded. Give them a larger measured limit, and avoid a value so tight that healthy slow runs are cancelled.

Verify on your repo

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to audit and fix this practice in your own repo.

Prompt for your coding agent
Audit every workflow under .github/workflows for runner-executing jobs missing `timeout-minutes`. Add a job-level bound to each job with `runs-on`, sized from recent successful runtime data plus conservative headroom rather than copying one arbitrary value everywhere. Do not add `timeout-minutes` to reusable-workflow caller jobs that use `jobs.<job_id>.uses`, because GitHub does not allow that key there; when the caller references a workflow in this repository, audit the runner-executing jobs inside the called workflow instead. For test processes, integration calls, or network steps that can hang, consider a tighter step-level `timeout-minutes` while keeping the whole-job bound as the fallback. Do not leave legitimately long runner jobs unbounded; give them a larger measured limit. Preserve workflow behavior, show the diff, explain how each timeout was sized, and open a PR rather than applying it blindly.

Ground these changes in the upstream docs before you edit: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idtimeout-minutes, https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepstimeout-minutes. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.

Prefer to check by hand?

  1. Parse every file under .github/workflows/ and flag each runner-executing jobs.<job_id> mapping with no timeout-minutes key. Exempt caller jobs with uses:; when they call a workflow in this repository, audit the executable jobs in that file instead.

  2. Check hang-prone test and network steps for a deliberate step-level bound where it can fail faster than the whole job.

  3. Compare each value with recent healthy run durations, including the long tail, and confirm the limit leaves enough headroom for normal variance.

Automate this with StarSling.

Install the StarSling GitHub App. It moves supported jobs onto StarSling runners, then AI agents keep inspecting your workflows and opening optimization PRs you review.

More best practices for GitHub Actions

Where to go next in the CI best-practices catalog.

All CI best practices

Trying to make GitHub Actions faster?

If you do not know why a run is slow yet, start with the diagnostic guide. If your workflows are already optimized but still slow, see our guide to fast GitHub Actions and GitHub Actions runner alternatives. Building containers in CI? The Docker workflow guide covers layer caching end to end.

FAQ

What is the default GitHub Actions job timeout?

A GitHub Actions job defaults to 360 minutes, or six hours, when timeout-minutes is omitted. Set an explicit lower value based on the job's healthy runtime so a hang fails much sooner.

How do I set a timeout on a GitHub Actions job vs a step?

Put timeout-minutes beside runs-on in the job mapping to bound the whole job. Put it beside run or uses in one step to bound only that step. Use the job limit as the safety net and a tighter step limit for the operation most likely to hang.

What happens when a job hits timeout-minutes?

GitHub automatically cancels the timed-out job and stops its remaining steps. Downstream jobs then follow their existing needs and conditional rules for a dependency that did not succeed.

What is a good timeout-minutes value?

Use recent successful runtimes, including the long tail, then add conservative headroom. A healthy job should finish comfortably inside the limit; the timeout should catch a real hang, not ordinary variance.

Sources

1GitHub Actions · job timeout-minutes (opens in new tab)

2GitHub Actions · step timeout-minutes (opens in new tab)

Last updated 2026-07-21