GitHub

GitHub Actionsのconcurrency|groupの設計とcancel-in-progress・queueの選び方

GitHub Actionsのconcurrency|groupの設計とcancel-in-progress・queueの選び方

concurrencyは、同じグループに属する実行を同時に走らせないための制御です。1行足すだけで連続pushの無駄打ちが止まり、デプロイの追い越しも防げますが、グループ名の組み立てを誤ると関係のないワークフローまで巻き添えで止まります。この記事ではワークフロー単位とジョブ単位の違い、groupの文字列設計、cancel-in-progressを真にしてよい条件、2026年5月に加わったqueueプロパティ、そして課金分数への跳ね返りまでを扱いました。ワークフローの基本構造はGitHub Actionsとは?できること・使い方とCI/CD自動化の解説記事、パイプライン全体の考え方はCI/CDとは?仕組み・パイプライン・導入すべき企業の判断基準を先に押さえてください。

まとめ:concurrencyで先に決める4つの設計

第一に、停止の範囲はgroupの文字列がすべて決めます。同じ文字列になった実行は、リポジトリ内のどのワークフローから来たものでも同じ枠を奪い合います。逆に文字列が1文字でも違えば別枠として扱われました。${{ github.workflow }}を先頭へ入れるかどうかだけで、巻き添えの範囲は「そのワークフローだけ」と「リポジトリ全体」に分かれます。

第二に、書ける場所は2階層あり、参照できるコンテキストが違います。ワークフロー単位のconcurrencyで使えるのはgithub・inputs・varsの3つだけで、ジョブ単位のjobs.<job_id>.concurrencyではこれにneeds・strategy・matrixが加わりました。マトリックスの値でグループを分けたいなら、置き場所はジョブ単位しかありません。

第三に、cancel-in-progressは「途中で止めても何も壊れない実行」にだけ使います。テストやlintは打ち切っても取り直せますが、デプロイやマイグレーションを中間状態で止めると復旧作業が発生しました。デプロイ系は打ち切らず、順番に流す側へ倒してください。

第四に、順番に流したいならqueueを指定します。既定のsingleでは待機できる実行が1本だけで、後から来た実行が先に待っていた実行を取り消します。2026年5月に加わったmaxを指定すると、同一グループで最大100本まで順番待ちができるようになりました。ただしcancel-in-progress: trueとの併用は許されず、書くと検証エラーになります。

concurrencyがワークフロー単位とジョブ単位の2階層で効く仕組み

最初に、どこへ書くと何が止まるのかを整理します。ここが曖昧なまま書き足すと、意図しないジョブが待たされる原因を追えません。

同じgroup名を持つ実行が既定で1本しか同時に走らない仕組み

concurrencyはgroupとcancel-in-progressの2つを取ります。公式ドキュメントは「同じconcurrencyグループを使う実行は、同時には最大1本しか走らない」と定義しており、この1本という枠がすべての挙動の土台です。

on:
  push:
    branches:
      - main

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

この形はワークフロー名とrefの組でグループを作ります。同じブランチへ続けてpushすると、走っていた実行が取り消されて新しい実行が始まりました。ブランチが違えばグループ文字列も違うので、他のブランチのCIは巻き添えになりません。

グループ名の判定は大文字と小文字を区別しない扱いです。Deploy-Prodとdeploy-prodは同じグループとして扱われるため、複数のワークフローで手書きした文字列がたまたま一致して、想定しない直列化が起きることがあります。

ワークフロー単位とジョブ単位で参照できるコンテキスト範囲の違い

書ける場所は2つあります。ファイル直下に置けばワークフロー実行そのものが対象で、jobs.<job_id>の下に置けばそのジョブだけが対象です。この2つは効く範囲だけでなく、式で参照できるコンテキストも違いました。

書く場所 使えるコンテキスト
ワークフロー直下 github, inputs, vars
ジョブの下 左記+needs・strategy・matrix

実務で効いてくるのはmatrixとneedsの有無です。ジョブ単位なら前段ジョブの出力でグループを組み立てられますが、ワークフロー単位ではイベント情報とリポジトリ変数しか手元にありません。「デプロイ先の環境名で直列化したい」という要件は、環境名がneeds由来ならジョブ単位へ置くことになります。前段ジョブから値を受け取る経路そのものはneedsとoutputsによるジョブ間の受け渡しで整理しました。

matrixを含むジョブへ素のgroupを書くと全セルが直列化する

ここが事故の起きやすい場所です。マトリックスジョブへジョブ単位のconcurrencyを付けるとき、グループ文字列にmatrixの値を含めないと、展開された全セルが同じグループへ入ります。2OS×3バージョンの6セルが1本ずつ順番に走る形になり、所要時間はほぼ6倍へ伸びました。

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-24.04, windows-2025]
        node: [20, 22, 24]
    runs-on: ${{ matrix.os }}
    concurrency:
      group: test-${{ github.ref }}-${{ matrix.os }}-${{ matrix.node }}
      cancel-in-progress: true

セルごとに独立した枠を与えるなら、グループ文字列へマトリックス変数をすべて含めます。逆に「外部の共有環境を叩くセルだけは直列にしたい」なら、含める変数を意図的に絞ってグループを粗くしてください。セルの展開規則と組み合わせの調整はGitHub Actionsのマトリックスビルド|組み合わせ展開とinclude・excludeの調整で扱いました。

concurrency groupの文字列設計で停止させる範囲が決まる

グループ文字列は、事実上この機能の設計そのものです。よく見かける3つの型と、それぞれが持つ副作用を押さえます。

github.workflowを外すと別のワークフローまで巻き添えになる

group: ${{ github.ref }}のようにワークフロー名を含めない書き方は、同じブランチで走るすべてのワークフローを1つの枠へ押し込みます。CIとリントとドキュメント生成を別ファイルへ分けていても、同じrefで動く限りは1本ずつしか進みません。

意図してそう組む場面もあります。同じ外部サービスのサンドボックスを叩く複数のワークフローを、リポジトリ全体で1本に絞りたいときです。この場合はグループ名をshared-sandboxのような固定文字列にして、refも含めないほうが目的に合いました。停止範囲を広げたいのか狭めたいのかを先に決めてから、文字列へ何を入れるかを選んでください。

head_refはpull_request以外で空になるため代替を用意する

github.head_refはpull_requestイベントでのみ定義されます。pushとpull_requestの両方で発火するワークフローへ素で書くと、push時にグループ文字列が空になり、そのワークフローのpush実行がすべて同じ枠へ集まりました。

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

公式ドキュメントが示すフォールバックはgithub.run_idです。run_idは実行ごとに一意なので、pull_request以外のイベントでは実質的に「グループ分けしない」状態になります。PRの連続pushだけを打ち切りたいときの定石はこの形です。どのイベントで発火させるかの設計はGitHub Actionsのトリガー(on)|イベント選定とbranches・pathsフィルタの設計と合わせて決めます。

group名の大文字と小文字は区別されないので命名を統一する

先に触れたとおり、グループ名は大文字小文字を無視して照合されます。複数人でワークフローを足していくと、Deploy-Stagingとdeploy-stagingが別ファイルに生まれ、意図せず同じ枠を共有する事態が起こりました。

命名は小文字とハイフンへ寄せ、先頭に用途を置く規則にしておくと衝突を追跡しやすくなります。ci-で始まるものは打ち切ってよい検証系、deploy-で始まるものは打ち切らない適用系、と接頭辞で意味を持たせておけば、レビュー時にcancel-in-progressの値が妥当かどうかを文字列だけで判断できます。

cancel-in-progressで止めてよい実行と止めてはいけない実行

cancel-in-progressは既定でfalseです。真にするかどうかは好みではなく、その実行が中断されたときに何が残るかで決まります。

検証だけのワークフローなら途中で打ち切っても不整合が残らない

テスト、lint、型検査、ビルド確認のように「結果を読むだけ」の実行は、打ち切っても外部に痕跡を残しません。PRへ5回続けてpushしたとき、古い4回分の結果は誰も読まないので、走らせ続ける理由がありませんでした。

この用途ではcancel-in-progress: trueを入れて構いません。開発者が待つ時間も短くなり、消費される分数も減ります。ただし、テストの途中でアーティファクトを外部ストレージへ書き出す構成だと、中断で半端なファイルが残る場合があります。書き出し先が共有領域なら、打ち切り前提の設計にしないでください。

デプロイと本番のマイグレーションは途中で打ち切ってはいけない

デプロイ中の打ち切りは、旧バージョンと新バージョンが混在した状態を残します。データベースのマイグレーションが途中で止まれば、スキーマとアプリケーションの版がずれたまま稼働することになりました。この状態からの復旧は、手作業のロールバックか再適用のどちらかで、いずれも人手が要ります。

デプロイ系のワークフローはcancel-in-progress: falseのまま、グループを環境名で固定して順番に流します。切り替え自体を無停止で行う手順はゼロダウンタイムデプロイとは?無停止を壊す発生源と設定順序・計測方法で扱っています。承認や待機を挟んで人が止められるようにする設計はGitHub ActionsのEnvironments|承認ゲートと環境別シークレットの設計の側の役割で、concurrencyとは別の層です。

cancel-in-progressに式を書いて条件付きで打ち切る

cancel-in-progressには真偽値だけでなく式も書けます。参照できるコンテキストはgroupと同じで、github・inputs・varsです。

concurrency:
  group: ci-${{ github.head_ref || github.run_id }}
  cancel-in-progress: ${{ github.ref_name != 'main' }}

この書き方なら、フィーチャーブランチのCIは打ち切り、mainブランチのCIは最後まで走らせる、という使い分けが1つのワークフローで成立します。mainの実行結果をリリース判定へ使っている場合、途中で消えると判定材料が失われるので、この分岐は実務でよく要りました。cancel-in-progressへ書く式そのものの規則はGitHub Actionsの条件分岐(if)|書く場所で変わるコンテキストと評価の規則で整理しています。

queueにmaxを指定して待ち行列を100本まで伸ばす設定

2026年5月7日のGitHub Changelogで、同一グループに待機できる実行の本数を増やすqueueプロパティが加わりました。それ以前と挙動が変わる部分なので、既存のワークフローを読み直す価値があります。

既定のsingleでは後から来た実行が先の待機を取り消してしまう

Changelogは従来の挙動を「1つのグループには実行中1本と待機中1本しか置けず、もう1本が入ってくると待機中のものが取り消されて置き換わる」と説明しています。queueを書かないときの既定値singleは、この従来挙動そのものでした。

デプロイを直列化する目的でグループを固定していた場合、この挙動は取りこぼしを生みます。3件のマージが短時間に続くと、1件目が実行中、2件目が待機、3件目の到着で2件目が取り消され、結果として2件目の変更だけがデプロイされないまま残りました。3件目に2件目のコミットが含まれていれば実害は出ませんが、環境ごとに対象が違う構成では抜けが起こりえます。

queueにmaxを与えると最大100本まで順番待ちができる

queue: maxを指定すると、同一グループで最大100本まで待機できます。上限を超えた分は取り消される点が変わらないので、無制限の待ち行列ではありません。処理は先入れ先出しで進みますが、公式ドキュメントは開始時刻の厳密さまでは保証していないと注意しています。

concurrency:
  group: deploy-production
  queue: max
  cancel-in-progress: false

この設定はデプロイやリリース通知のように「1件も落としたくないが同時には走らせたくない」処理に向きます。順番待ちの列が伸びる代わりに、取りこぼしはなくなりました。

maxとcancel-in-progressの併用は検証エラーになる

queue: maxとcancel-in-progress: trueを同時に書くことは許されていません。公式ドキュメントは「この組み合わせは許可されず、ワークフローの検証エラーになる」と明記しています。Changelogも、増えた待ち行列が使えるのはcancel-in-progressがfalseまたは未設定のときだと述べていました。

仕様として筋は通っています。maxは「全部順番に流す」意思表示で、cancel-in-progress: trueは「古いものは捨てる」意思表示なので、両立しません。設定を書くときは、この2つのどちらの思想でそのグループを運用するのかを先に決めてください。

concurrencyがキュー滞留と課金分数へ与える影響を見積もる

concurrencyは無料の安全装置ではありません。待たせる設計にも打ち切る設計にも、それぞれ別のコストが付きました。

打ち切っても消費済みの分数は請求から消えないという課金の前提

GitHub Actionsの課金はジョブ単位の実行時間で計測され、1分未満の端数は1分へ切り上げられます。打ち切られたジョブも、開始から取り消しまでの時間は計上されました。cancel-in-progress: trueが減らすのは「これから消費される分」だけで、既に走った分は戻りません。

効き目が大きいのは、実行時間が長くpush頻度も高いワークフローです。20分かかるE2Eテストが1日に10回打ち切られていれば、削減できる分数はまとまった量になります。逆に1分で終わるlintへ入れても、切り上げの都合でほとんど変わりませんでした。分単価と無料枠の実額から効果を見積もる手順はGitHub Actionsの料金|2026年改定後の分単価・無料枠とコスト削減の判断基準にまとめています。

直列化でキュー待ち時間がデプロイ間隔の下限になる運用上の制約

queue: maxで直列化すると、1件あたりの所要時間がそのままデプロイの最小間隔になります。デプロイに12分かかるなら、5件のマージが同時に来たとき最後の1件が終わるのは60分後です。マージ頻度が上がるほど列は伸び、「マージしたのに反映されない」という問い合わせが増えました。

列が慢性的に伸びる状態は、直列化の粒度が粗すぎるサインです。環境ごと、サービスごとにグループを分けられないかを先に検討してください。モノレポで全サービスのデプロイが1つのグループへ入っているなら、パスフィルタでワークフローを分割し、グループもサービス単位へ割るほうが筋の良い直し方になります。

同時実行上限とconcurrencyのキューは別々の待ち行列

ジョブが待たされる理由は2種類あります。1つはconcurrencyによるグループ内の待機、もう1つはプランごとの同時実行ジョブ数の上限に張り付いている状態です。前者はYAMLで解けますが、後者はランナーの手当てでしか解けません。

切り分けは実行画面の待機理由の表示で判断できます。concurrencyによる待機はグループの存在とともに示され、枠の上限による待機は単にランナーの割り当て待ちとして残ります。後者が慢性化しているなら、大型ランナーやセルフホストランナーへの切り替えが検討対象で、判断材料はGitHub Actionsのruns-on|ラベル指定の記法と-latest更新・arm64への備えに整理しました。

concurrencyを入れてよい条件と入れないほうがよい場面

ここからは判断を言い切ります。concurrencyは効き目が見えやすいぶん、要件を確かめずに全ワークフローへ配ってしまいがちな設定です。

プルリクエスト単位のCIには入れてリリース発行には入れない判断基準

入れるべき筆頭は、PR単位で回る検証系のワークフローです。グループは${{ github.workflow }}と${{ github.head_ref || github.run_id }}の組、cancel-in-progressは真。この1点だけで、連続pushの無駄打ちは止まります。導入の判断に迷う要素はほとんどありません。

入れないほうがよいのは、タグのpushで動くリリース発行やパッケージ公開です。これらは実行ごとに対象が違い、後から来た実行が前の実行を代替しません。打ち切れば1つのバージョンが世に出ないまま終わります。順番だけ守りたいならcancel-in-progress: falseとqueue: maxの組でグループを固定し、打ち切りは選ばないでください。

concurrencyより先にジョブ分割やパス絞り込みを試す

CIが遅い、費用が高いという課題に対して、concurrencyは3番手の打ち手です。先に効くのは、そもそも動かさない設計と、動かす量を減らす設計でした。

  1. パスフィルタとブランチ条件で、関係のない変更では発火させない
  2. 依存の取得結果をキャッシュして、毎回のセットアップ時間を削る
  3. 重いジョブを分割し、失敗の早いものを前段へ置く
  4. それでも残る無駄打ちをconcurrencyで打ち切る

順序を逆にすると、打ち切りで数字が改善したように見えて、根本の実行時間には手が入りません。キャッシュのkey設計と復元されない原因の切り分けはGitHub Actionsのキャッシュ|actions/cacheのkey設計と復元されない原因の切り分けで扱っています。

排他が業務要件になったらconcurrencyの外へ出す判断

見送るべき場面もあります。「同時に走ってはいけない」が業務上の要件になっているとき、つまり在庫の更新や課金処理のような、二重実行が金銭的な損失へ直結する処理です。

concurrencyはGitHub Actionsの実行を並べる仕組みであって、分散ロックではありません。ワークフロー外から同じ処理が叩かれれば素通りしますし、実行が異常終了したときの排他の解放も保証されていません。この種の要件は、アプリケーション側の冪等化か、外部のロック機構で担保してください。concurrencyは「うっかり二重に走らせない」ための衛生設備として使い、正しさの保証は別の層へ置くのが安全な線引きです。CI基盤の実行制御を棚卸ししたい、あるいは運用を引き取ってほしいという相談は、保守運用 / 内製化支援で承っています。

よくある質問

concurrencyはワークフローとジョブのどちらへ書くべきですか?

止めたい単位で決めます。ワークフロー全体を1本に絞るならファイル直下、特定のジョブだけを直列化するならジョブの下です。matrixやneedsの値でグループを分けたい場合は、それらのコンテキストがジョブ単位でしか使えないため、置き場所は自動的にジョブの下になります。

cancel-in-progressの既定値はtrueですか?

いいえ、既定はfalseです。書かなければ実行中のものは止まらず、新しい実行が待機側へ回ります。打ち切りたい場合は明示してください。式を書いてブランチごとに切り替えることもできます。

queueにmaxを指定すると何本まで待たせられますか?

同一concurrencyグループあたり100本までです。上限を超えて到着した実行は取り消されます。queueを書かないときの既定はsingleで、待機できるのは1本だけでした。

queueのmaxとcancel-in-progressのtrueは一緒に使えますか?

使えません。公式ドキュメントはこの組み合わせを許可しないと明記しており、書くとワークフローの検証エラーになります。増やした待ち行列が有効になるのはcancel-in-progressがfalseか未設定のときです。

concurrencyで打ち切ればCIの費用は下がりますか?

下がる場面は限られます。課金はジョブ単位の実行時間で計測され、打ち切りまでに消費した分は計上されるためです。効くのは実行時間が長くpushの頻度も高いワークフローで、数十秒で終わるジョブへ入れても切り上げの都合で差はほとんど出ません。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.09 テックブログ IDCFクラウド(IDCフロンティア)不正アクセス・ランサムウェア:影響先・復旧・データは戻るか
  2. 2026.10.08 テックブログ 大阪公立大学のランサムウェア被害と仮想化基盤の停止|全授業休講に至った経緯とバックアップを守る設定
  3. 2024.11.08 テックブログ OpenAPI GeneratorでJavaコードを自動生成する方法|CLI導入からSpring・ライブラリ選択まで
  4. 2026.10.09 テックブログ ニッスイのサイバー攻撃で日水物流の入出荷停止|委託先クラウド障害に荷主が備える手順
  5. 2026.10.09 テックブログ 京王電鉄のランサムウェア被害とグループ共通基盤:決済・ポイント・予約が止まった範囲と遮断の初動

RELATED POSTS 関連記事

目次