Skip to content
Back to Quality Assurance

Concurrency Control & Pipeline Cancellation

Limit concurrent pipeline runs and cancel superseded runs so CI resources are spent on the latest commit, not on stale commits that will be pushed over.

Why

Without concurrency control, a developer who pushes 5 commits in quick succession triggers 5 pipeline runs, all running in parallel against commits 1-4 that are already obsolete — the CI burns 5x the compute for results no one will read, and the 5th run (the one that matters) queues behind the 4 stale runs and is slower. Concurrency control cancels the stale runs when a new commit is pushed, so only the latest commit's pipeline runs — this halves the CI bill, speeds up feedback for the latest commit, and frees runners for other PRs. The same applies to deploy pipelines: if a deploy is in progress and a new deploy is triggered, the old one should be cancelled (not run concurrently, which can corrupt the release). The mechanism is a concurrency group keyed by the branch (or PR), with cancel-in-progress: true.

When

Whenever CI runners are a shared, finite resource — which is essentially always. The control becomes critical when the team has more than a few concurrent PRs or when CI minutes are a billed resource.

How

1. Add a concurrency group to each workflow
   - Key by the branch (or PR number) so runs on different branches do not cancel each other
   - Set cancel-in-progress: true so a new run cancels the previous run on the same branch

2. Use a different group for PR vs push
   - PR runs: concurrency group "pr-${{ github.event.pull_request.number }}"
   - Main runs: concurrency group "main" (do not cancel — every main run is a deploy candidate)

3. Cancel stale matrix jobs
   - A matrix build (e.g. 5 OS × 3 Node versions = 15 jobs) that is cancelled mid-run frees 15 runners
   - The concurrency group cancels the whole matrix when a new commit is pushed

4. Do NOT cancel deploy pipelines
   - Deploy pipelines use cancel-in-progress: false (a deploy in progress should complete, not be cancelled mid-deploy)
   - Or use a separate concurrency group for deploys with a max of 1

5. Monitor the cancellation rate
   - A high cancellation rate (> 50%) means developers push too frequently — consider a pre-commit hook that runs lint locally
   - A low cancellation rate (< 5%) means the concurrency control is not catching stale runs — check the group key

Example

# .github/workflows/ci.yml — cancel stale PR runs
name: ci
on:
  pull_request:
    branches: [main]
concurrency:
  group: pr-${{ github.event.pull_request.number }}
  cancel-in-progress: true
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

# .github/workflows/deploy.yml — do NOT cancel deploys
name: deploy
on:
  push:
    branches: [main]
concurrency:
  group: deploy-production
  cancel-in-progress: false   # let the current deploy finish

Common Problems

  • ⚠Keying the concurrency group on github.ref for PR runs — all PRs share the same ref (refs/pull/N/merge), so one PR's run cancels another PR's run, and developers see their CI disappear with no explanation.
  • ⚠Setting cancel-in-progress: true on the deploy workflow — a production deploy is cancelled mid-deploy by a new push to main, leaving the service in a half-deployed state with mixed old and new code.
  • ⚠Not using concurrency control at all — a developer pushes 10 commits during a debugging session, 10 pipeline runs execute in parallel, the CI bill spikes, and the 10th run (the one that matters) is slow because the runners are busy with the 9 stale runs.
  • ⚠Cancelling a run that is in its final seconds (uploading artifacts, posting a comment) — the cancellation loses the artifacts and the comment, and the developer has to re-run the pipeline to get the test report.

Best Practices

  • ✓Key the PR concurrency group on the PR number (github.event.pull_request.number), not on github.ref — this ensures each PR's runs only cancel that same PR's stale runs.
  • ✓Set cancel-in-progress: true for CI workflows (lint, test, build) and false for deploy workflows — CI runs are cheap and repeatable, deploys are expensive and must complete atomically.
  • ✓Add a short grace period before cancellation (if the platform supports it) so a run in its final seconds can finish uploading artifacts — this preserves the test report for the latest run.
  • ✓Monitor the cancellation rate and the runner utilization together — a high cancellation rate with low utilization indicates the group key is wrong (runs are not being cancelled when they should be).