GitHub Actions Concurrency Groups: Cancellation, Queues, and Safe Deployment Serialization
Control GitHub Actions job overlap, pending-run replacement, and deployment queues with precise concurrency groups, cancellation rules, and safe workflow design.
GitHub Actions concurrency groups control which workflow runs or jobs in a repository may execute at the same time. They are useful for avoiding duplicate preview work, canceling obsolete branch checks, and preventing overlapping deployments to one environment. They are also easy to misconfigure: a group that is too broad can cancel an unrelated workflow, while a group that uses cancellation for production can interrupt a release that should have completed safely.
Treat concurrency as a repository-scoped scheduling rule for Actions work, not a general-purpose distributed lock. It does not stop a human from deploying through another tool, serialize a database migration outside the group, or guarantee that a canceled process has undone side effects. Pair it with protected environments, deployment health checks, idempotent automation, and an explicit rollback procedure.
Choose workflow-level or job-level scope
At workflow level, the concurrency group covers the workflow run. At job level, it applies only to that job; other jobs in the same run may continue while the constrained job waits. Use workflow-level concurrency when a newer commit should replace an older end-to-end run. Use job-level concurrency when tests and builds can proceed in parallel but a shared deployment target must be serialized.
The group name is the coordination key. Runs with the same key in the same repository can interact even when they come from different workflow files. Include enough context to isolate the resource you mean to protect. For branch CI, a useful pattern is the workflow name plus the Git ref. For production deploys, a stable environment-specific key may be appropriate if every deploy to that environment must share one queue.
name: CI
on:
push:
branches: [main, "release/**"]
pull_request:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/verify.sh
For pull requests, github.ref usually identifies the synthetic pull-request ref. If you instead build a group from github.head_ref, remember that this value is only defined for pull-request events. Provide a safe fallback when the same workflow also handles pushes or other events:
concurrency:
group: ci-${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
This example creates a distinct group for a push where head_ref is empty. Verify group names against every trigger handled by the workflow, including manual dispatch, scheduled runs, tags, and merge queues if those are enabled in the repository.
Understand the default pending-run replacement
Without a queue setting, a concurrency group permits one running item and at most one pending item. When another item for the same group is queued, it replaces and cancels the existing pending item. This behavior is often desirable for branch CI: the latest commit supersedes work that has not started yet. It can be surprising for deployments or release workflows where every approved run must execute.
cancel-in-progress: true additionally cancels the currently running item in the same group when a new one arrives. That is appropriate for interruptible work such as linting or building a pull-request preview. It is risky for stateful deploy steps, database migrations, infrastructure changes, or workflows whose cleanup is not safe to interrupt. GitHub-hosted cancellation stops the Actions run; it does not prove that an external deployment system reverted a partially applied change.
For sequential deployments that should wait instead of replacing pending releases, the current syntax supports queue: max:
name: Production deployment
on:
push:
branches: [main]
concurrency:
group: deploy-production
queue: max
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- run: ./scripts/deploy-production.sh
The documented maximum queue is 100 pending items; once full, additional arrivals are canceled. queue: max and cancel-in-progress: true are incompatible because one says to keep waiting work and the other says to cancel active work. Select the policy intentionally and test how manual reruns, hotfixes, and abandoned releases behave when the group is occupied.
The documentation describes queued work as FIFO by the time an item starts waiting, while also warning that ordering is not guaranteed because dispatch and start times can vary. Do not use concurrency as a strict release-order ledger. If deployment order is a correctness requirement, validate the artifact or commit at deploy time and use a release system that explicitly enforces sequencing and rejects stale versions.
Prevent accidental cross-workflow cancellation
Because group names are shared, a generic value such as production can make multiple workflows coordinate with one another. That may be intended when a single production environment must never receive concurrent deployments. It may be a mistake when one group is used by a reusable workflow, a hotfix pipeline, and a rollback workflow that should not cancel one another.
For CI cancellation limited to a single workflow and ref, use both workflow identity and ref, for example ci-${{ github.workflow }}-${{ github.ref }}. For production, decide whether different workflow files deploy the same underlying environment. If yes, a common environment key can protect the target; if not, include a stable component identifier. Review changes to workflow names because github.workflow contributes to the group string and a rename can create a second independent concurrency key.
GitHub treats group names as case-insensitive. Normalize generated values and avoid relying on capitalization to separate Prod from prod. Keep keys short and explicit enough to inspect in the Actions UI or concurrency-group API. Do not embed secrets in a group name; group names are coordination metadata, not a secret store.
Put cancellation only around work that can be discarded
Branch verification is usually safe to cancel when a newer commit arrives, provided steps do not publish non-idempotent external artifacts before completion. Preview deployments may be cancelable if each run owns an isolated preview environment and teardown is reliable. Production deployments are generally not safely cancelable halfway through unless the deployment system has a tested transaction or rollback mechanism.
For a workflow with parallel test jobs and a single deployment job, job-level concurrency narrows the lock to the side effect:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: ./scripts/test.sh
deploy:
needs: test
runs-on: ubuntu-latest
environment: production
concurrency:
group: deploy-production
queue: max
steps:
- run: ./scripts/deploy.sh
This allows independent test jobs to run while deployment jobs wait in the shared group. It does not make the deployment atomic. The deploy script should verify that the artifact is still eligible, apply changes idempotently where possible, report health, and fail clearly if another control plane has changed the target.
Observe queue behavior and cancellation effects
For each group, define what should happen when a new run arrives while another is running and while one is already pending. Exercise both cases in a test repository or non-production environment. Inspect run cancellation reasons, skipped or canceled jobs, deployment records, and external system state. A green workflow summary does not prove that a canceled deployment left no partial resource change.
Where queueing is enabled, monitor queue age and length, rejected or canceled items, and the age of the artifact being deployed. A long queue may mean the deployment system is unavailable or releases are arriving faster than they can be safely applied. Define when operators may supersede a queued item, how hotfixes bypass or enter the queue, and how a rollback obtains the same serialization guarantee as a forward deploy.
GitHub’s REST API can expose the active concurrency groups and their current runs for supported repositories and permissions. Use it as operational evidence, not as a replacement for workflow design or an external deployment audit trail. Preserve deployment identifiers, commit SHAs, artifact digests, and environment approvals in the release record.
Production review checklist
- Does the group represent the actual shared resource: a branch check, preview, staging environment, or production target?
- Is the group scoped so unrelated workflows cannot cancel each other accidentally?
- Should a new run cancel active work, replace only pending work, or queue instead?
- Can the action being canceled leave an external system partially modified?
- Does the workflow validate that the queued artifact is still safe to deploy when it eventually starts?
- Are environment protection rules, approvals, least-privilege credentials, health checks, and rollback independent of the concurrency key?
- Have both a second pending run and an active-run cancellation been tested?
- Is a separate non-GitHub deployment path capable of bypassing this Actions-only coordination?
Use cancellation to save time on obsolete, repeatable checks. Use bounded queues to serialize approved side effects when every queued change must be considered. For critical deployments, keep the concurrency key, approval boundary, artifact identity, and rollback behavior explicit; never assume that a scheduler lock alone makes a release safe.
Related:
- How to Set Up a CI/CD Pipeline with GitHub Actions
- GitHub Actions OIDC: Short-Lived Cloud Credentials Without Repository Secrets
Sources: