Best practice · Trigger Scope

Cancel superseded runs with concurrency groups

Add a GitHub Actions `concurrency` group scoped to the ref with `cancel-in-progress: true`, so a new push cancels the now-obsolete run instead of leaving both to occupy runners.

Do this

PR workflows declare a top-level concurrency group keyed on the workflow and ref, with cancel-in-progress: true. When a developer pushes again, the superseded run is cancelled instead of running to completion for a commit no one is waiting on. The group is broad enough (per-workflow-per-ref) that it doesn't serialize unrelated jobs.1

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' }}

Avoid this

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.

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.

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.
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.

More best practices for GitHub Actions

Where to go next in the CI best-practices catalog.

All CI best practices

FAQ

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 (opens in new tab)

Last updated 2026-07-06

Get started

Stop wasting CI minutes on commits no one is waiting on.

One line to install. Faster runs on day one, and agents that open reviewable PRs to keep your pipeline following practices like this one.