---
title: "GitHub Actions timeout-minutes: bound runner jobs"
description: "Set timeout-minutes on GitHub Actions runner jobs so a hung job is killed fast instead of burning up to the 360-minute default. Copyable YAML."
url: https://starsling.dev/best-practices/github-actions/bound-job-timeouts
canonicalUrl: https://starsling.dev/best-practices/github-actions/bound-job-timeouts
---

# Bound GitHub Actions runner jobs with timeout-minutes

Best practice: Runner & Queue. Last updated: 2026-07-21

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.

## Table of contents

- [Do this: bound the job and hang-prone steps](#do-this)
- [Avoid this: rely on the 360-minute default](#avoid-this)
- [Shipped by StarSling](#shipped-by-starsling)
- [Job-level vs step-level timeout-minutes](#job-level-vs-step-level-timeouts)
- [Why it matters](#why-it-matters)
- [When to set timeout-minutes](#when-to-use)
- [Verify on your repo](#verify)
- [Trying to make GitHub Actions faster?](#speedup-guides)
- [Sources](#sources)

<a id="do-this"></a>

## 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.

_A bounded job with a tighter test-step timeout_

```yaml
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
```

<a id="avoid-this"></a>

## 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_

```yaml
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
```

<a id="shipped-by-starsling"></a>

## 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.

### mastra-ai/mastra#14098: chore(ci): add timeout-minutes to all StarSling-hosted workflow jobs

StarSling added measured timeout bounds to every previously unbounded StarSling-hosted job so stuck runs release their runners instead of reaching GitHub's six-hour default.

Diff: secrets.e2e.yml, secrets.test-combined-stores.yml, and secrets.test-memory.yml add timeout-minutes to the check-changes and test jobs that run on StarSling.

Source: [mastra-ai/mastra#14098](https://github.com/mastra-ai/mastra/pull/14098), merged 2026-04-13.
Customer story: [Mastra](https://starsling.dev/customers/mastra).

<a id="job-level-vs-step-level-timeouts"></a>

## 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.

<a id="why-it-matters"></a>

## 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.

<a id="when-to-use"></a>

## 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.

<a id="verify"></a>

## Verify on your repo

Hand this prompt to your coding agent to audit and fix this practice in your own repo:

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:

- 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.
- Check hang-prone test and network steps for a deliberate step-level bound where it can fail faster than the whole job.
- Compare each value with recent healthy run durations, including the long tail, and confirm the limit leaves enough headroom for normal variance.

<a id="more-best-practices"></a>

## More best practices for GitHub Actions

- [Cut CI queue time in GitHub Actions](https://starsling.dev/best-practices/github-actions/cut-queue-time)
- [Cancel superseded runs with concurrency and cancel-in-progress](https://starsling.dev/best-practices/github-actions/cancel-superseded-runs)
- [All CI best practices](https://starsling.dev/best-practices/github-actions)

<a id="speedup-guides"></a>

## 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.

- [GitHub Actions too slow](https://starsling.dev/github-actions-too-slow)
- [Fast GitHub Actions](https://starsling.dev/fast-github-actions)
- [GitHub Actions alternatives](https://starsling.dev/github-actions-alternatives)
- [Docker CI on GitHub Actions](https://starsling.dev/ci/docker)

<a id="faq"></a>

## 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.

<a id="sources"></a>

## Sources

1. [GitHub Actions - job timeout-minutes](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idtimeout-minutes)
2. [GitHub Actions - step timeout-minutes](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepstimeout-minutes)
