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:
- Navigate to your repository and click Actions.
- Select the workflow from the left sidebar.
- Click the name of the
queuedorin progressrun that is misbehaving. - 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:
- Go to the repository and click Actions.
- In the left sidebar, click the workflow you want to disable.
- 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.ymlWhen 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 testYou 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 Plan | Standard Runner Concurrency | macOS Concurrency | Included Monthly Compute |
|---|---|---|---|
| GitHub Free | 20 | 5 | $12 USD |
| GitHub Pro | 40 | 5 | $18 USD |
| GitHub Team | 60 | 5 | $18 USD |
| GitHub Enterprise | 500 | 50 | $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_TOKENexpires 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: trueA few rules apply when writing concurrency groups:
- Concurrency group names are case insensitive, so
prodandProdare 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_refis only defined onpull_requestevents; if the workflow also triggers onpush,${{ 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, andvarscontexts. If you configure concurrency at the job level (jobs..concurrency), you can also useneeds,strategy, andmatrix. - The
concurrencyproperty supports aqueueparameter that defaults tosingle(allowing at most one pending job or run). It also acceptsmax, which allows up to 100 pending runs. But beware: combiningqueue: maxwithcancel-in-progress: truecauses 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