on
Stop wasting CI minutes: use concurrency to cancel redundant GitHub Actions runs
Continuous integration is most useful when it gives fast, relevant feedback. But when contributors push a flurry of commits or update a pull request repeatedly, GitHub Actions can queue many runs that test old commits — wasting minutes, slowing feedback, and increasing costs. The concurrency feature in GitHub Actions gives you a simple way to ensure only the most recent run for a branch or PR proceeds while older, redundant runs are canceled automatically. This quick guide explains what concurrency does, how cancellation behaves, and how to add it safely to a CI workflow.
Why concurrency matters for CI
- Reduces wasted CI minutes: Canceling outdated runs prevents work on commits that are already superseded by newer commits. This matters for GitHub-hosted runners and for teams using limited self-hosted capacity. (docs.github.com)
- Speeds useful feedback: Developers get results for the latest commit sooner because older runs don’t clog the queue.
- Avoids accidental overlapping deployments: When used around deployment workflows, concurrency prevents concurrent deploys to the same environment.
How concurrency works (the essentials)
- The concurrency keyword groups workflow runs or jobs by a string (the concurrency group). You can supply a static string or build one dynamically with workflow expressions. (docs.github.com)
- For a given concurrency group, GitHub Actions allows at most one running and one pending run at a time. When a new run is queued in the same group, any existing pending run is canceled and the new run takes its place. You can also tell GitHub to cancel currently running runs (not just pending ones) by using cancel-in-progress: true. (docs.github.com)
- Ordering of runs in the same concurrency group is not guaranteed, so design your concurrency keys carefully. (docs.github.com)
A minimal example: cancel previous CI runs on a branch Add concurrency near the top-level of the workflow file (right after on:). This example cancels any in-progress or pending workflow runs for the same branch, so only the latest push to main is executed.
name: CI
on:
push:
branches: [ main ]
concurrency:
group: ci-$
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "run tests"
This groups runs by the branch reference (github.ref) and cancels older runs for that branch when newer ones start. The behavior and syntax are documented in the GitHub Actions concurrency docs. (docs.github.com)
Canceling per-pull-request (useful for PR checks) Pull requests use different contexts. A common pattern is to group by github.head_ref so that updates to the same PR cancel previous checks. Because github.head_ref is only defined for pull_request events, include a fallback to avoid syntax errors when the workflow may run for other events:
concurrency:
group: $
cancel-in-progress: true
This keeps PR runs unique per branch name (head ref). If head_ref is undefined, run_id ensures uniqueness and avoids an expression error. (docs.github.com)
Job-level concurrency You can apply concurrency at the job level if you want to limit concurrency for a particular job rather than the whole workflow. This is handy if only one job (for example, deployment) must be serialized while other tests can run in parallel.
jobs:
deploy:
runs-on: ubuntu-latest
concurrency:
group: deploy-$
cancel-in-progress: true
steps:
- run: echo Deploying...
Be aware: job-level concurrency groups are evaluated independently of workflow-level groups. Use distinct group names if you don’t want different concurrency scopes to interact. (docs.github.com)
What cancellation actually does (and what it doesn’t) When a workflow or job is canceled, GitHub follows a defined cancellation process:
- The server re-evaluates if conditions for running jobs/steps; jobs whose if conditions evaluate to true might continue instead of being canceled.
- The server notifies runners to cancel the steps, and the runner sends a SIGINT to the step’s entry process; if that process doesn’t exit in ~7.5 seconds, a SIGTERM is sent. After a five-minute cancellation timeout, any still-running jobs are forcibly terminated. (docs.github.com)
Implications and gotchas
- Long-running steps may still run for a short time after cancellation (due to the graceful shutdown sequence). If your CI runs substantial cleanup or side-effectful commands, consider making those steps idempotent or guarded by conditions. (docs.github.com)
- Don’t reuse overly broad concurrency keys across unrelated workflows. If multiple workflows share a group name, a new run in one workflow can cancel runs in another. Use workflow-specific tokens like github.workflow in the group name to isolate behavior. (docs.github.com)
- Concurrency is not a replacement for better test design. For maximum benefit, pair concurrency with fast, focused test suites and caching so the single allowed run finishes quickly.
- Ordering is not guaranteed. If your workflow depends on strict ordering of runs (rare for CI), concurrency may not be a fit. (docs.github.com)
Useful patterns
- Per-branch CI (keep only latest run per branch): group = ci-$, cancel-in-progress: true.
- Per-PR CI (keep only latest PR run): group = pr-$, cancel-in-progress: true.
- Only cancel pending runs but not currently running ones: omit cancel-in-progress or set it to false (or a conditional expression) to preserve running runs while removing queued ones.
- Conditional cancel-in-progress: you can pass an expression, for example, cancel only on non-release branches: cancel-in-progress: $. This allows selective cancellation based on branch naming conventions. (docs.github.com)
Practical tips to pair with concurrency
- Make tests fast: Use caching (actions/cache) and parallel test suites where appropriate so the single allowed run completes sooner.
- Preserve artifacts: If you want to keep test artifacts from canceled runs, upload them early using actions/upload-artifact; canceled runs that still reach the upload step will store artifacts. Consider uploading to a short-term storage if retention matters.
- Protect deployments: For deployment workflows, use a narrower concurrency group (for example, include environment or workflow name) to avoid cross-workflow cancellations.
Short checklist before enabling concurrency on a repo
- Identify the workflows where redundant runs cause the most waste (PR checks, branch CI).
- Decide the grouping key: branch-level, PR-level, environment-level, or workflow-specific.
- Choose cancel-in-progress behavior: cancel pending runs only, or cancel running runs too.
- Test on a small repository or a single workflow to verify behavior and edge cases (especially if you use self-hosted runners).
Summary Adding concurrency to your GitHub Actions CI is a low-friction, high-impact change: a small YAML block prevents redundant runs, saves minutes, and speeds feedback for developers. Use dynamic concurrency group names to scope cancellation to branches or PRs, include cancel-in-progress when you want the newest commit to win, and watch for the cancellation behavior details so long-running cleanup steps behave correctly. The official GitHub Actions docs describe the syntax and cancellation process in detail. (docs.github.com)
References
- GitHub Actions: Concurrency (concepts and usage). (docs.github.com)
- Control the concurrency of workflows and jobs (syntax, examples, and expressions). (docs.github.com)
- Workflow cancellation reference (how cancellation signals are delivered and timeouts). (docs.github.com)
- Advanced workflow configurations: cancel-in-progress patterns and guidance. (resources.github.com)