The Operation Was Canceled in GitHub Actions
Last updated
The operation was canceled means GitHub stopped your job or step before it finished, and it did so on purpose: a newer run replaced it, a sibling job failed, a timeout hit, or someone clicked cancel. It rarely means your code is wrong. This page lists the causes by likelihood and shows how to tell them apart from the run page. Behaviour is from GitHub's workflow syntax reference, checked October 2026.
The exact message
Error: The operation was canceled.
Variants you may see next to it:
Canceling since a higher priority waiting request for 'ci-refs/heads/main' exists
Error: The run was canceled by @username.
The runner has received a shutdown signal.
Look at the annotations at the top of the run summary. The text there usually names the cause.
Cause 1: concurrency with cancel-in-progress
If a workflow declares concurrency with cancel-in-progress: true, pushing again cancels the earlier run of the same group. The cancelled job logs The operation was canceled.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
That is wanted for pull request CI. It is not wanted for deployments or release jobs, where cancelling mid-way can leave things half done. Also note GitHub's rule: by default, an existing pending run in the same group is replaced by the new queued one, even without cancel-in-progress. That is what the "higher priority waiting request" message means.
Fix for deploys:
concurrency:
group: deploy-production
cancel-in-progress: false
With false, one run goes and one waits; older waiting runs are still dropped. If you need every run to execute, use unique group names per run or a merge queue (see GitHub merge queue). Only cancel in CI jobs:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Cause 2: matrix fail-fast
In a matrix, fail-fast is true by default. When one cell fails, GitHub cancels the cells still running, and they report "The operation was canceled" even though they were fine. The real failure is in the one cell that is red, not the cancelled ones.
strategy:
fail-fast: false
matrix:
node: [18, 20, 22]
Set fail-fast: false while debugging so you see every cell's real result. See matrix builds.
Cause 3: timeouts
A step or job that exceeds timeout-minutes is cancelled. The step maximum is 360 minutes, and GitHub-hosted jobs are capped at 6 hours. Often a step hangs waiting for input or a network service. Read timeout-minutes behaviour and time limits. Set explicit limits so hangs fail fast:
jobs:
test:
timeout-minutes: 20
steps:
- run: npm test
timeout-minutes: 15
Cause 4: a person or API cancelled the run
Someone clicked Cancel workflow, or an automation called the API. Find out with:
gh run view RUN_ID --log | head -50
gh api repos/OWNER/REPO/actions/runs/RUN_ID --jq '.conclusion, .actor.login'
Bots such as release tooling and gh run cancel in other workflows are common.
Cause 5: runner shutdown or loss
Self-hosted runners on spot instances, or runner hosts that reboot, give The runner has received a shutdown signal and then cancelled steps. Hosted runners can also be reclaimed after an infrastructure issue. Use ephemeral runners with graceful draining, or fall back to hosted. If the log says communication was lost instead, read hosted runner lost communication.
Cause 6: a dependent job failed
A job with needs: is skipped, not cancelled, when its dependency fails. If you see cancellation, check for fail-fast or concurrency first. An if: always() cleanup step still runs on cancellation only if you used if: always() or if: cancelled():
- name: Clean up
if: always()
run: ./cleanup.sh
Step by step diagnosis
- Open the run summary, read annotations. They often say who or what cancelled.
- Check the workflow for
concurrency:. If present andcancel-in-progress: true, that is likely it. - Check for a matrix. If any cell failed, fix that one.
- Compare the job duration with
timeout-minutes. - If it only happens on self-hosted runners, check host logs for reboots.
How to prevent it
- Use per-ref concurrency groups, never one global group for all branches.
- Disable
cancel-in-progressfor deploy and release workflows. - Set
fail-fast: falsefor matrices where each cell is independent. - Add
timeout-minutesto every job and slow step. - Use
if: always()for cleanup that must run.
Cancelled runs still use minutes up to the moment they stop, which is why the concurrency approach saves money; see reduce your GitHub Actions bill. It also helps with queues, see Waiting for a runner. If you want shorter jobs so they are cancelled less often, compiler.dev runs existing jobs on 8 vCPU / 16 GB machines after a one-line runs-on change.
FAQ
Does cancel-in-progress cancel only older runs?
Yes. The newest run in a concurrency group continues, and runs of the same group already in progress are cancelled when cancel-in-progress: true.
Why did my passing matrix cells say canceled?
fail-fast defaults to true. One failing cell cancels the others still running.
Can I tell who canceled a run?
Yes, in the run summary annotation, and via the API field actor plus the audit trail for the repository.
Is a cancelled run billed?
You pay for the minutes used before cancellation.
Related
- API Rate Limit Exceeded in GitHub Actions: FixesFix "API rate limit exceeded for installation ID" in GitHub Actions: GITHUB_TOKEN allows 1,000 requests per hour per repo. Secondary limits and fixes.
- Cache Not Found for Input Keys: Causes and FixesCache not found for input keys in GitHub Actions is a cache miss, not always an error. Why it happens, how key and branch scope work, and fixes.
- Docker Pull Rate Limit in GitHub Actions: FixesFix "toomanyrequests: You have reached your pull rate limit" in GitHub Actions: Docker Hub limits, docker/login-action, mirrors and GHCR alternatives.
- Hosted Runner Lost Communication With the ServerFix "The hosted runner lost communication with the server" in GitHub Actions: causes like OOM, disk full and killed processes, with steps to diagnose.