- Published on
Why GitHub Actions Cancels Runs After Every Push
- Authors

- Name
- Mehdi Akiki
Article · Interrupted execution
GitHub Actions can cancel an older run when a new push enters the same concurrency group with cancel-in-progress: true. First check the group and trigger: the cancellation may be intentional, a collision between workflows, or a policy that keeps restarting a useful check.
Picture a pull request with a slow integration check and an agent that keeps spotting small improvements. The first push starts the check. A second fixes a name. A third improves an error message. A fourth updates a comment.
Each push cancels the previous run.
By lunchtime, the code looks better and no integration run has finished. The latest job is installing dependencies again.
The workflow hasn't broken. It is doing exactly what its cancellation rule says. The agent keeps submitting work faster than the workflow can finish checking it.
What cancellation saves and costs
Cancelling an old run can save useful resources. If a developer has replaced a broken implementation, another twenty minutes testing the discarded version may be wasted. Other people need those runners too.
The trouble starts when every run is replaced before it reaches the slow check that matters. The team pays for repeated setup and gets no completed integration result. Even a run that was nearly finished can disappear because someone pushed a comment fix.
There is work for the developer too. They have to decide whether a partial result still applies, compare old failures with new code, and work out which checks the latest commit still needs.
Before changing the workflow, open a few cancelled runs. Where did they stop? Did the next commit change the behavior under test? Was useful work discarded, or did the run fail early enough that cancellation saved most of the cost?
These details help decide whether to change the cancellation rule, the timing of pushes, or the order of checks. The same fix won't suit every workflow.
The agent can help with this investigation. It can list the cancelled runs and the commits that replaced them. That is more useful than immediately rewriting the workflow to allow every run to continue.
Keep unrelated pull requests apart
For a workflow triggered by pull requests, a group like this gives each request its own cancellation rule:
concurrency:
group: ${{ github.workflow }}-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
If the workflow also handles other events, it needs a fallback for events without a pull request number. Otherwise the group may not separate the work as intended.
GitHub's concurrency documentation explains how matching groups limit work and how cancel-in-progress: true can cancel a running member. A group shared by every pull request could make unrelated requests cancel each other.
Including the workflow name also separates workflows that validate the same request. Group names apply across the repository, so a PR number alone could make two different workflows cancel each other. This still leaves the request in our story cancelling its own earlier runs with every push.
For that, the developer can collect small cleanup edits before pushing them, or let one version finish its required checks before asking for more polish. The agent can still read code, explain a result, or prepare suggestions while it waits. It doesn't need to commit every observation immediately.
An early focused check helps too. Even if a later push replaces the full run, the developer may already have an answer about the small behavior they changed. The larger required checks still need to finish for the commit that will merge.
Don't copy the rule into production without thinking
A deployment has different risks. By the time it is cancelled, it may have uploaded files, changed a database, or started replacing a service. Stopping the job doesn't automatically undo those changes.
This site's production workflow lets the active deployment finish:
concurrency:
group: production-site-deployment
cancel-in-progress: false
This rule lets the active deployment finish, but the default queue holds only one pending run. A newer arrival can replace that waiting run.
GitHub now offers an alternative when you need more waiting releases:
concurrency:
group: production-site-deployment
cancel-in-progress: false
queue: max
This is an alternative, not this site's current setting. It allows up to 100 pending runs; further arrivals are cancelled. Combining queue: max with cancel-in-progress: true is invalid. Order follows when runs begin waiting for the group, which may differ from dispatch order. If every release must run in a strict order, those limits still need handling.
Choose the behavior before copying either fragment:
| Situation | Configuration or next action |
|---|---|
| Only the latest PR revision should finish | Give that workflow and PR their own group; cancellation may save discarded work |
| Two workflows cancel each other | Include a unique workflow identity in the group |
| An active deployment must finish | Use a deployment group with cancellation disabled |
| Several releases must wait | Consider the bounded queue, and handle its capacity and ordering limits |
| Every small cleanup push restarts integration | Change push timing or the check order; a unique group alone will not fix it |
The first fragment is for a PR-triggered workflow. If another event also triggers it, define how that event should be grouped instead of assuming a PR number exists. These examples show concurrency decisions; they do not replace runner, permission, or checkout configuration.
An agent changing this configuration should explain which behavior is wanted. “Only test the latest version of this pull request” and “let an active production release finish” need different choices.
For the pull request, first let one revision finish the required integration check. Use that result to decide whether the next push needs a repair or whether the optional cleanup can wait. Keep that result attached to the revision it actually checked; a completed older run is useful history, not verification of a newer parser edit.
After a successful run, another rename is a choice with a visible cost: another round of required checks. Sometimes it is worth doing. Sometimes it can wait for the next change. At least the developer now has a completed result to base that choice on.