Cancel superseded runs with concurrency and cancel-in-progress

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.

How StarSling works

AI agents open the PR

StarSling agents inspect your workflow, apply this optimization, and open a reviewable PR automatically.

Do this: concurrency group with cancel-in-progress

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

One live run per ref; cancel superseded PR runs
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: no concurrency block (cancel-in-progress defaults to false)

Each new push leaves the old run going, so obsolete commits keep occupying runners.

No concurrency: obsolete runs pile up
# 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' }}
Cancels pull-request runs only. Pushes, tags, and scheduled validation are left to finish, and the comment says why.
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' }}
The same conditional, with the concurrency group keyed per pull request so two branches never cancel each other.
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
The group is keyed per ref and cancellation switches on only for pull-request refs, so a rebuild supersedes its own branch and never a run on main.

Why it matters

If your run log says Canceling since a higher priority waiting request for followed by your concurrency group and the word exists, that is this feature working: a newer push superseded the run. If it does not say that, you have the opposite problem. 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 cancel-in-progress: true (and when not to)

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.

Prompt for your coding agent
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?

  1. Check PR-triggered workflows for a top-level concurrency: block: grep -rn 'concurrency' .github/workflows/.

  2. Flag pull_request/push workflows with no concurrency group, or one with cancel-in-progress: false.

  3. Confirm the group key includes github.ref (per-ref), not a global constant that would serialize every PR into one lane.

Go further

One fix, all of them, or forever.

You have the prompt for this one practice. Here is how much further you can take it, each step doing more for you than the last.

  1. Fix this one thing

    Copy the prompt above

    Hand OPT45 / OPT46 / OPT44 to your coding agent and fix it in your repo today.

  2. Fix everything, once

    Install the ci-score skill

    One prompt grades your whole workflow config against all 11 CI Score checks, this one included, and hands your agent a ranked fix for every gap it finds. Open source, runs locally. It grades configuration, not speed.

  3. Keep it fixed, forever

    Install the StarSling GitHub App

    Connect GitHub and the fixes stay applied as your CI evolves, with agents that 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.

All GitHub Actions best practices

More ways to improve GitHub Actions

If you do not know why a run is slow yet, start with the diagnostic guide. Let an agent find which of these applies: /ci-speedup. If your workflows are already optimized but still slow, see our guide to fast GitHub Actions and GitHub Actions runner alternatives. Building containers in CI? The Docker workflow guide covers layer caching end to end. Want to see how your configuration measures up before changing anything? CI Score grades workflow config against a pass/fail rubric of these practices. It is not a speed measurement. To find exploitable workflow paths before attackers do, ci-secure checks the ten critical GitHub Actions attack vectors and lets you choose which fixes to apply.

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

1GitHub Actions · concurrency syntax (opens in new tab)

2GitHub Actions · concurrency (opens in new tab)

Last updated 2026-08-19