GitHub Actions timeout

How to stop a runaway GitHub Actions workflow

To stop a runaway GitHub Actions job immediately, open the run in the GitHub Actions tab and click "Cancel workflow", or cancel it from your terminal using the GitHub CLI with repository write access. By default, GitHub lets any job run for up to 360 minutes (6 hours) on GitHub-hosted runners before terminating it, which will quickly burn through the included $12 to $18 monthly compute allowance on private repositories.

If a test hangs or an infinite loop sneaks into a build step, waiting for GitHub's default limits to kick in means losing hours of billable runner time. Here is how to kill active runs right now, shut off triggering workflows, and configure hard limits so runaway processes can't drain your account.

Cancel the running workflow immediately

If you see a job spinning right now, you need repository write access to stop it.

In the GitHub web interface:

  1. Navigate to your repository and click Actions.
  2. Select the workflow from the left sidebar.
  3. Click the name of the queued or in progress run that is misbehaving.
  4. Click Cancel workflow in the upper-right corner.

If you are automating cancellations inside a custom action, your GITHUB_TOKEN needs the actions: write permission to cancel runs via the API.

For steps running as background processes inside a job (background: true), you can have up to 10 background steps running at once. If one hangs, target it with the cancel step keyword using its id. The runner sends a SIGTERM signal so the process can exit cleanly, followed by a SIGKILL after a short grace period if it doesn't shut down.

Disable the workflow so it stops queueing runs

Cancelling a live run doesn't stop new ones from starting if webhooks or developers keep pushing commits. Disabling the workflow prevents it from triggering again without deleting your YAML file from .github/workflows.

Keep one detail in mind: disabling a workflow does not cancel its existing runs. You must cancel active runs separately, and you won't be able to re-run those runs or their jobs while the workflow remains disabled.

To disable it in the browser:

  1. Go to the repository and click Actions.
  2. In the left sidebar, click the workflow you want to disable.
  3. Open the dropdown menu on the right and click Disable workflow.

If you prefer the command line, use the gh workflow disable command. Pass the workflow name, ID, or file name:

gh workflow disable build.yml

When you are ready to turn it back on later, run gh workflow enable build.yml.

Cut the default 360-minute job timeout

The biggest financial trap in GitHub Actions is jobs..timeout-minutes. If you don't define this property, GitHub defaults it to 360 minutes.

That is 6 full hours. On private repositories, standard GitHub-hosted runners are free and unlimited only for public repositories; private repositories draw down your monthly dollar allowance ($12 on GitHub Free, $18 on Pro and Team) and bill at per-minute rates after that. Letting an unattended job spin for 6 hours can wipe out an entire month of included compute in an afternoon.

Add timeout-minutes to every single job in your repository. Set it to a number that accommodates a normal run plus a small buffer, such as 10 or 15 minutes:

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - name: Run test suite
        timeout-minutes: 10
        run: npm test

You can also add timeout-minutes to individual steps. The step-level limit accepts a maximum value of 360 minutes for both GitHub-hosted and self-hosted runners. If your job-level timeout-minutes exceeds the execution time limit for the runner, the job gets canceled when the runner's own ceiling is met instead.

Runner execution limits across GitHub plans

GitHub enforces hard caps on how long jobs run and how many jobs can run at the same time. The total concurrent jobs you can run on standard GitHub-hosted runners depends directly on your GitHub plan.

GitHub PlanStandard Runner ConcurrencymacOS ConcurrencyIncluded Monthly Compute
GitHub Free205$12 USD
GitHub Pro405$18 USD
GitHub Team605$18 USD
GitHub Enterprise50050$300 USD

Outside of plan concurrency, GitHub enforces global time limits regardless of settings:

  • GitHub-hosted runners: Each job in a workflow has a maximum execution time limit of 6 hours. When a job hits this mark, it is terminated and fails.
  • Self-hosted runners: Each job can run for up to 5 days before it is terminated and fails. Queued jobs on self-hosted runners can wait in line for up to 24 hours before GitHub automatically cancels them.
  • Overall workflow runs: The maximum run time limit is 35 days per workflow run, which includes execution duration, waiting, and environment gate approvals. Environment gate approvals can hold a workflow for up to 30 days.
  • Queue and event caps: Repositories have a trigger rate limit of 1,500 events per 10 seconds, and a queued limit of 500 workflow runs per 10 seconds. Beyond that, incoming webhook-triggered runs are blocked.
  • Token expiration: The standard GITHUB_TOKEN expires when a job finishes or after 24 hours. If you run long jobs on self-hosted runners that exceed 24 hours, the token will expire before the job completes.

Auto-cancel redundant runs with concurrency

When developers push several commits in quick succession, queued and running jobs pile up. You don't need three separate CI runs testing outdated commits on the same pull request.

Use the concurrency key with cancel-in-progress: true. When a new run is queued, it cancels any currently running job or workflow in the same concurrency group.

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

A few rules apply when writing concurrency groups:

  • Concurrency group names are case insensitive, so prod and Prod are treated as identical.
  • Make group names unique across different workflows in the same repository by prefixing them with ${{ github.workflow }}. If two separate workflow files share the same group name, one workflow will cancel the in-progress runs of the other.
  • Use a fallback for the group key. The context github.head_ref is only defined on pull_request events; if the workflow also triggers on push, ${{ github.head_ref }} will be empty. Using ${{ github.head_ref || github.run_id }} ensures pull requests share a branch-specific group while standard branch pushes get a unique fallback ID.
  • Workflow-level concurrency expressions can only access github, inputs, and vars contexts. If you configure concurrency at the job level (jobs..concurrency), you can also use needs, strategy, and matrix.
  • The concurrency property supports a queue parameter that defaults to single (allowing at most one pending job or run). It also accepts max, which allows up to 100 pending runs. But beware: combining queue: max with cancel-in-progress: true causes a workflow validation error.

Watch out for matrix builds and schedules

Matrix builds can multiply runner usage quickly. A job matrix configured via jobs..strategy.matrix can generate up to 256 jobs per workflow run on both GitHub-hosted and self-hosted runners.

GitHub maximizes parallel execution based on runner availability, but if you don't limit it, a broken matrix can consume all 20 of your Free runner slots instantly. Use max-parallel to constrain how many matrix jobs execute simultaneously.

Always verify that fail-fast is active:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: true
      max-parallel: 4
      matrix:
        node: [18, 20, 22]
        os: [ubuntu-latest, windows-latest]

jobs..strategy.fail-fast defaults to true. When enabled, GitHub cancels all remaining in-progress and queued matrix jobs the moment any single job fails. If you set fail-fast: false, a failing setup step will keep running across all 256 matrix permutations, burning minutes for tests that are already doomed.

Scheduled workflows (on.schedule) can also quietly burn minutes. They run in UTC by default on the latest commit on the default branch, and the shortest interval you can configure is once every 5 minutes. If a 5-minute scheduled workflow runs on private runners and takes 8 minutes to complete, runs will overlap and exhaust your concurrency pool.

For public repositories, GitHub automatically disables scheduled workflows if there has been no repository activity for 60 days, and forks disable schedules by default.

Catch runaway jobs with Costbrake

If you want a safety net across your billing accounts, you can monitor your spend with Costbrake. It watches what you spend across 23 cloud, AI, and SaaS platforms, including AWS, Azure, Google Cloud, and GitHub.

Costbrake monitors running Actions jobs priced at list rates alongside your billing usage report. On the Pro plan ($12 a month) and Team plan ($39 a month), checks run every 5 minutes. A spike alert triggers if an hour costs more than 3x the usual 7-day baseline and at least $5 more than usual.

It also includes an opt-in brake for GitHub. A brake never triggers on its own; an admin has to click and confirm it. For GitHub Actions, Costbrake provides two brakes:

  • Disable workflow: Disables the specific workflow to block new runs and cancels all currently queued and running runs in one click.
  • $0 Actions budget: Sets the organization's Actions budget to $0 with Stop usage enabled, preventing any repository from launching jobs on GitHub-hosted runners past your plan's included usage.

You can wire it up using a read-only fine-grained personal access token with Organization > Administration: Read and Repository > Actions: Read. To configure it automatically with your AI tooling, check out the agent setup instructions. For broader bill protections, you can also read our guide on how to stop GitHub Actions from billing past your budget.

Questions and answers

What is the default timeout for a GitHub Actions job?

The default value for jobs.<job_id>.timeout-minutes is 360 minutes (6 hours). If you do not specify a timeout in your workflow YAML, GitHub will allow the job to run up to that 6-hour limit on GitHub-hosted runners before terminating it.

How do I cancel running workflows and stop new ones?

You can cancel individual queued or in progress runs in the Actions tab by opening the run and clicking Cancel workflow (requires repository write access), or automatically via an action granted actions: write permission on GITHUB_TOKEN. To stop new runs from starting, disable the workflow in the UI or via gh workflow disable <WORKFLOW>, note that disabling a workflow only stops future triggers and requires you to cancel existing runs separately.

What happens when I disable a GitHub Actions workflow?

Disabling a workflow stops it from triggering on any future push, pull request, or schedule event without deleting the file from your repository. It does not cancel runs that are already queued or running, and you cannot re-run those jobs until the workflow is re-enabled.

Can a step timeout be set longer than the job timeout?

You can configure jobs.<job_id>.steps[].timeout-minutes up to 360 minutes on both GitHub-hosted and self-hosted runners. However, if any step or job timeout exceeds the execution time limit of the runner (6 hours on GitHub-hosted runners), the job is canceled as soon as the runner's execution ceiling is met.

Why did my concurrency setting fail to validate?

Combining cancel-in-progress: true with queue: max is not permitted by GitHub Actions and triggers a workflow syntax validation error. If you use cancel-in-progress: true, leave the queue at its default single behavior.

Sources

Updated