You push a fix, spot a typo a second later, and push again. GitHub cheerfully runs both. The first run, building a commit you've already replaced, grinds through your entire CI matrix while you sit waiting on the second. On a busy pull request that fans out to a dozen jobs, a real slice of your Actions minutes goes to building commits that were dead on arrival. The fix is one block of YAML, and it's the single most common thing missing from the repos I scan.
The one block
Drop this at the top level of a workflow file, alongside on: and jobs::
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group buckets runs by workflow and branch. When a new run starts in a bucket that already has one going, cancel-in-progress: true kills the old one. Push twice and only the latest survives, instead of both running to the end.
What the group key actually does
github.ref is the branch (or, on a pull request, that PR's ref), so every branch and every PR gets its own bucket. Pushing to your PR cancels your previous run and touches nobody else's. Keeping github.workflow in the key stops separate workflows (CI vs lint vs docs) from cancelling each other.
One rule: never put github.sha or github.run_id in the group key. They are unique per commit and per run, so every run lands in its own bucket and nothing ever cancels. That's the number-one reason a concurrency block "isn't working." To make the key read as the branch name on pull requests (forks included), use ${{ github.workflow }}-${{ github.head_ref || github.ref }}.
Concurrency on a single job
The block can live on a job, not just the whole workflow, which is how you serialize one step (a deploy) while everything else still fans out:
jobs:
deploy:
runs-on: ubuntu-latest
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false # never two deploys at once
steps:
# ...
Top-level concurrency: governs the whole run; job-level governs that one job. Reach for job-level when only the deploy needs to be one-at-a-time and the tests can keep fanning out in parallel.
The one place you do NOT want cancel-in-progress
Deploy, release, and publish workflows. Cancelling a deploy halfway can leave a half-pushed image or a partially published package. For those you still want a concurrency group, so two never run at once, but with cancellation OFF, which queues them instead:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false # serialize deploys, never interrupt one
Same logic for your default branch: you usually want every commit to main built and deployed, not cancelled by the next merge. A common pattern cancels only on branches and PRs:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
Why it's worth the thirty seconds
A cancelled run is minutes you don't spend. On private repos and self-hosted runners that's a direct line on the bill; on a public repo it's still faster feedback and less queue contention, since a superseded run isn't tying up a runner your real build is waiting for. Three lines, the cheapest reliability win in Actions, and almost nobody sets it.
Common questions
Does it cancel matrix jobs too? Yes. A matrix run is still one workflow run, so a superseded run cancels every matrix leg at once, not one at a time.
Why isn't my concurrency cancelling anything? Almost always the group key is too unique, usually a github.sha or github.run_id that snuck in, so no two runs ever share a bucket. Or cancel-in-progress is missing (it defaults to false).
Can I queue runs instead of cancelling them? Yes, set cancel-in-progress: false. GitHub keeps one run going and at most one waiting, cancelling older pending runs but never the one in flight. That's the right mode for deploys and releases.
Find out if you're missing it
It's the most common gap I see, big repos included. prettier, for instance, has no concurrency group on its test workflow. See real 30-day Actions scorecards on the showcase, then scan your own.
I build GitSpider, which scans a repo's GitHub Actions and flags exactly which of these patterns apply, with the fix for each. Free, no install for public repos: gitspider.com/scan.
Top comments (0)