Cancel superseded runs with concurrency groups
Add a GitHub Actions `concurrency` group scoped to the ref and turn on `cancel-in-progress` for PR runs (it defaults to `false`), so a new push cancels the now-obsolete run instead of leaving both to occupy runners.
AI agents open the PR
StarSling agents inspect your workflow, apply this optimization, and open a reviewable PR automatically.
Do this
PR workflows declare a top-level concurrency group keyed on the workflow and ref, with cancel-in-progress set. Here is what the key actually does. It defaults to false: the run already in progress finishes, and the new run sits as pending until it does. Set it to true and the in-progress run in that group is cancelled immediately, so the new commit starts right away. That is what you want on a PR, where the superseded run is testing a commit no one is waiting on, and what you do not want on main or a release workflow, where every commit's run may deploy or publish and must complete. Scoping the value to the event (rather than a bare true) gives you both. The group itself is broad enough (per-workflow-per-ref) that it doesn't serialize unrelated jobs.12
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Cancel superseded runs on PRs; let main / release runs finish.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# On a workflow that ONLY runs on pull_request, the plain literal is fine:
#
# concurrency:
# group: ${{ github.workflow }}-${{ github.ref }}
# cancel-in-progress: true
#
# The default is cancel-in-progress: false, which lets the in-progress run
# finish while the new run waits as pending. Never set the literal true on a
# workflow that also runs on push to main or on a release tag.Avoid this
Each new push leaves the old run going, so obsolete commits keep occupying runners.
# No concurrency block. Every push to a PR starts another full run,
# and the superseded ones keep occupying runners until they finish.Seen in the wild
Grafana, Moby, and Next.js use concurrency groups so stale pushes stop burning runner time after a newer run supersedes them.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Cancel stale PR runs without interrupting push, tag, scheduled, or
# manually dispatched validation.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}concurrency:
# Limit concurrent runs to 1 per PR, but allow concurrent runs on canary branch
group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}-{2}', github.workflow, github.ref_name, github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}Why it matters
Without a concurrency group, rapid pushes to a PR leave several full runs racing, every obsolete one still burning a runner until it ends. Cancelling superseded runs reclaims that compute and frees capacity for runs that matter. Keep the group scoped to the ref, not a global constant, an over-narrow or over-broad group is its own problem (it can serialize or cancel jobs you didn't mean to).
When to use
Use it when
PR- and branch-push workflows where developers commonly push several times in quick succession.
Be careful when
Do not use cancel-in-progress: true on the default branch or release workflows where every commit must complete (each may deploy or publish). Scope cancellation to PRs; let main runs finish.
Verify on your repo
Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to audit and fix this practice in your own repo.
Inspect this repo's .github/workflows for a top-level `concurrency:` block on `pull_request` and `push` workflows. Flag any that have none, or one with `cancel-in-progress: false`, so obsolete runs pile up on rapid pushes. Add a `concurrency` group keyed on `${{ github.workflow }}-${{ github.ref }}` with `cancel-in-progress` scoped to PRs, and make sure the default branch and release workflows still let every run finish (never set `cancel-in-progress: true` on `main`). Confirm the group includes `github.ref` so it does not serialize unrelated PRs into one lane. Show me the diff and open a PR rather than applying it blindly.
Ground these changes in the upstream docs before you edit: https://docs.github.com/en/actions/concepts/workflows-and-actions/concurrency. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.Prefer to check by hand?
Check PR-triggered workflows for a top-level
concurrency:block:grep -rn 'concurrency' .github/workflows/.Flag
pull_request/pushworkflows with no concurrency group, or one withcancel-in-progress: false.Confirm the group key includes
github.ref(per-ref), not a global constant that would serialize every PR into one lane.
Automate this with StarSling.
Install the StarSling GitHub App. It moves supported jobs onto StarSling runners, then AI agents keep inspecting your workflows and opening optimization PRs you review.
More best practices for GitHub Actions
Where to go next in the CI best-practices catalog.
- Runner & Queue
Cut CI queue time in GitHub Actions
GitHub-hosted runners cap how many jobs your account can run at once (20 on Free, more on paid plans), so past that ceiling jobs queue for a free slot no matter how your workflows are written.
- Trigger Scope
Filter workflows by path in GitHub Actions
Add
pathsorpaths-ignoreto expensive workflows so a docs-only or unrelated change doesn't trigger the full test suite.
FAQ
What does cancel-in-progress do, and what is its default?
cancel-in-progress decides what happens to the run already going in a concurrency group when a new one arrives. It defaults to false, so the in-progress run finishes and the new run waits as pending. With cancel-in-progress: true, GitHub cancels the in-progress run in that group immediately and starts the new one. Nothing else changes: the group key still decides which runs share a lane.
Where does the concurrency block go, workflow or job level?
Put it at the top level of the workflow so it governs the whole run. Job-level concurrency is for finer control, but for cancelling superseded PR runs the workflow-level group keyed on github.ref is what you want.
Is it safe to cancel in-progress runs on main?
No. On the default branch or a release workflow, every commit typically needs its run to complete, cancelling could skip a deploy or publish. Scope cancel-in-progress: true to PR refs and let main runs finish.
Sources
Last updated 2026-07-16