Concurrency Group

A concurrency group is a named lane in GitHub Actions that runs one workflow run or job at a time, and can cancel the run already in progress.

A concurrency group is a named lane in GitHub Actions that allows one workflow run or job at a time. Work that arrives while the lane is busy becomes pending, and the workflow can ask GitHub to cancel whatever is already running so the newest arrival takes the lane immediately.

The name is the whole mechanism. Anything in the repository that resolves to the same group string shares the lane, and anything that resolves to a different string runs without waiting.

Definition

A concurrency group is created by the concurrency key in workflow YAML. The key takes a group value, which is the group name, plus two optional settings that decide what happens to work already in the group. GitHub documents the behavior in the workflow syntax reference, checked on 2026-08-13.

The key sits at one of two levels, and the level decides what occupies the lane:

  • At the top level of a workflow file, the group governs whole workflow runs.
  • Inside a job, under jobs.<job_id>.concurrency, the group governs that one job across runs.

The group name can be a literal string or an expression. The allowed expression contexts differ by level: the workflow level accepts github, inputs, and vars, while the job level also accepts needs, strategy, and matrix.

The three settings

KeyValuesDefaultWhat it decides
groupstring or expressionrequired, no defaultThe identity of the lane. The same string means the same lane.
cancel-in-progressboolean or expressionfalseWhether a new arrival cancels the run already occupying the lane.
queuesingle or maxsingleHow many arrivals may wait: one under single, up to 100 under max.

queue: max and cancel-in-progress: true cannot be combined. GitHub rejects that workflow with a validation error, because one setting asks to preserve work in flight while the other asks to end it.

The states a group produces

At most one run or job in a group is in progress at any moment. A second arrival becomes pending. What happens to a third depends on queue:

  • Under queue: single, the pending item is cancelled and the new arrival takes the waiting slot, so the group holds one running item and one waiting item.
  • Under queue: max, up to 100 items wait and are processed first in first out by the time each one started waiting on the group rather than the time it was dispatched. Arrivals beyond 100 are cancelled.

Two further properties catch people out. Group names are case insensitive, so prod and Prod resolve to one group. And membership is scoped to the repository rather than to a workflow file, so two different workflow files whose group expressions produce the same string end up sharing a single lane.

A concurrency group never makes work start sooner. It removes superseded work and serializes work that must not overlap. Time spent waiting because every runner is busy is a different problem with different symptoms.

Example

The common shape keys the group on the branch ref and cancels the run in progress, so a new push replaces the check that is already running:

name: test
on:
  push:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

Including github.workflow in the group keeps this file's lane separate from other workflow files in the repository. github.ref gives every branch its own lane, so a push to a feature branch leaves a run on main alone.

Trace three pushes to the same branch, arriving 30 seconds apart, against a suite that takes 6 minutes:

ClockEventRun ARun BRun C
0:00Push 1 queues run Ain progressnot creatednot created
0:30Push 2 queues run Bcancelledin progressnot created
1:00Push 3 queues run Ccancelledcancelledin progress
7:00Run C reportscancelledcancelledsuccess

Only the newest commit gets a full test cycle. Under those assumptions the three runs consume 30 seconds plus 30 seconds plus 6 minutes of runner time, which is 7 minutes instead of the 18 minutes the same three pushes would consume with no group at all. The commit that ships is also the only one anybody reads a result for.

A group that queues instead of cancelling

Deploys want the opposite behavior. Put the group on the job, give it a literal name so every run of that job shares one lane, leave cancel-in-progress at its default, and let arrivals line up:

jobs:
  deploy:
    needs: unit-tests
    runs-on: ubuntu-latest
    concurrency:
      group: production-deploy
      queue: max
    steps:
      - run: ./scripts/deploy.sh

A rollout in flight now finishes, and the next deploy starts when the lane clears. Because the group is on the job rather than on the workflow, the test job in the same run is unaffected and keeps running in parallel with deploys from other runs.

Fallbacks in the group expression

An expression that references a property defined only for some events needs a fallback. github.head_ref exists on pull_request events and is empty elsewhere, so a workflow that also runs on push writes the group as ${{ github.head_ref || github.run_id }}. The run ID is unique per run, so runs from other events each land in a private lane instead of colliding in an empty group name.

FAQ

What does the concurrency group name control?

The group name is the identity of the lane. Any workflow run or job in the same repository that resolves to the same string shares that lane and waits for it. GitHub treats the name as case insensitive, so prod and Prod are one group.

Does a concurrency group make jobs start sooner?

No. A concurrency group only holds work back or cancels it. One run occupies the lane, the next becomes pending, and later arrivals are cancelled or lined up depending on the queue setting. Waiting caused by a shortage of runners is a separate problem.

How many runs can wait in one concurrency group?

One by default, because queue is set to single and each new arrival cancels the pending run it replaces. Setting queue to max lets up to 100 runs wait in the group, and arrivals beyond that are cancelled. queue max cannot be combined with cancel-in-progress true.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.