Skip to content
compiler.dev

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

  1. Open the run summary, read annotations. They often say who or what cancelled.
  2. Check the workflow for concurrency:. If present and cancel-in-progress: true, that is likely it.
  3. Check for a matrix. If any cell failed, fix that one.
  4. Compare the job duration with timeout-minutes.
  5. If it only happens on self-hosted runners, check host logs for reboots.

How to prevent it

  1. Use per-ref concurrency groups, never one global group for all branches.
  2. Disable cancel-in-progress for deploy and release workflows.
  3. Set fail-fast: false for matrices where each cell is independent.
  4. Add timeout-minutes to every job and slow step.
  5. 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.