Skip to content

GitHub Actions queues: stop stale deployments

A deployment queue preserves waiting work. It still needs a release contract that rejects an older artifact after a newer one is active.

Dopstack Engineering

· 8 min read

An original conceptual release diagram shows release 204 reaching the active target before the slower 203, which stops at a guard instead of replacing the newer release.

Quick answer

GitHub Actions concurrency can serialize participating deployments, and queue: max can retain multiple pending jobs. Neither establishes commit order at the target. Parallel builds can enter the queue in a different order. Record an immutable release identity and check it against the target’s current state before promotion; handle stale releases, repeats and in-flight operations explicitly.

GitHub Actions can serialize deployment jobs while still allowing an older release to run after a newer one. Its queue is ordered by when work starts waiting on the concurrency group, not by the workflow’s dispatch time. Choose the pending-retention policy, then verify the candidate against the target’s current release before promotion. [1]

Why can a queued deploy move production backward?#

Parallel preparation can invert the order in which deployment jobs reach the queue. A later release with a shorter build may wait first, deploy first and finish before an earlier build is ready. A queue that correctly preserves both requests can therefore retain a request you should no longer promote. That is a consequence of the documented group-wait ordering, not evidence of a broken scheduler. [1]

Scenario: an illustrative invoice service assigns monotonically increasing release sequences in one release authority before starting builds. Sequence 203 and sequence 204 refer to immutable artifacts. The build for 204 finishes first, reaches the production deployment group and becomes active. The slower 203 build arrives afterward. We want to preserve its disposition without letting it regress the target. No workflow or provider deployment was executed for this example.

Point in the caseObserved release stateRequired decision
Both builds started203 and 204 have distinct immutable identitiesKeep preparation parallel; sequence comes from the release authority
204 reaches the group firstTarget still runs an earlier releasePermit 204 only after verifying its approval and target precondition
204 becomes activeTarget records sequence 204 and its artifact digestTreat this as the new promotion baseline
203 reaches the group later203 is older than the active 204Record 203 as stale; do not promote it
Operator needs an older artifact againThis is an intentional rollback requestCreate a new approved release intent referencing that artifact
Worked decision record; hypothetical states, not execution results

A Git commit hash identifies source; it does not supply a sortable business release sequence. Neither a lexical hash comparison nor the order in which two independent workflows happen to obtain run numbers establishes the contract above. Pick one release authority for the target, or use a different explicit predecessor/approval model when releases cannot share a monotonic order.

Which concurrency group protects the target?#

Use a key that names the shared resource all participating deployment jobs can change. Within one repository, deployment paths to the same production target must agree on that key. Including the workflow name or branch can accidentally separate writers that need to wait for one another. GitHub documents repository-scoped groups and case-insensitive names. [1]

deployment-job-fragment.yaml
# Fragment for a trusted deployment job in one repository.
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    concurrency:
      group: invoice-production
      queue: max
      cancel-in-progress: false
    steps:
      - name: Integration point
        run: |
          echo "Replace this step with the reviewed promotion controller."
          exit 1

This fragment intentionally fails at the integration point. It does not invent a deploy.sh that might conceal the hard part. Add it to a workflow that has prepared and approved the immutable candidate, then connect it to a promotion controller that can verify and conditionally change the actual target. The group must cover the job that waits for the remote operation’s outcome, not merely a CLI invocation that submits work and immediately exits.

Job-level concurrency leaves independent preparation outside this serialization point. Workflow-level concurrency instead gates the run as a whole. Inspect both levels: an outer cancellation policy can still interrupt a run that contains a protected deployment job. If several repositories or an operator can write to the same target, they also need shared coordination outside this repository group. [1][3]

Should waiting deployments be retained or replaced?#

Use the policy that matches the release intent. By default, a same-group arrival replaces existing pending work. Setting cancel-in-progress to false does not turn that pending slot into a retain-every-request queue. Current github.com documentation supports queue: max for up to 100 pending jobs or runs, canceling additional arrivals once full. GitHub announced the larger-queue capability on May 7, 2026. [1][2]

For a disposable preview, only the newest snapshot may matter. For separately approved production releases, silently replacing a waiting request may violate the process even when running work is protected. Monitor the pending limit, record canceled arrivals and check the active release before promotion. It is not durable intake or an exactly-once promotion guarantee.

latest-pr-checks-fragment.yaml
# Separate pull-request validation workflow.
on:
  pull_request:
concurrency:
  group: pr-checks-${{ github.workflow }}-${{ github.event.pull_request.number }}
  queue: single
  cancel-in-progress: true
# Add the repository's existing validation jobs.

Here superseded validation is intentionally replaceable, so running cancellation is a deliberate choice. Do not paste this policy around an irreversible production change. The documentation rejects queue: max combined with cancel-in-progress: true; those settings do not create a retain-all-and-interrupt mode. Check the syntax supported by your actual GitHub platform before adopting the fragments, particularly on GitHub Enterprise Server. [1]

What should the target check before promotion?#

It should reconcile the candidate’s approved identity with an authoritative target record while holding the target’s promotion authority. For the illustrative monotonic-release contract, reject older sequences, recognize a repeat only when identity matches, stop on identity collisions and hold while a remote operation is unresolved. The code below makes those decisions visible. It does not perform network calls, acquire a lock or implement a database transaction.

promotion_decision.py
def promotion_decision(candidate, active, *, pending=False):
    # Example contract: one authority assigns unique, increasing sequences.
    # Both records must already be authenticated and schema-validated.
    if pending:
        return "hold: reconcile operation"
    if candidate["sequence"] < active["sequence"]:
        return "reject: stale sequence"
    if candidate["sequence"] == active["sequence"]:
        if candidate["digest"] == active["digest"]:
            return "repeat: already active"
        return "reject: identity conflict"
    return "advance: conditional promotion required"

Inputs are validated records with a positive integer sequence and an immutable artifact digest. Missing, malformed or unauthenticated records must stop before calling this function. A digest equality check cannot establish that the artifact is approved or correctly signed; those are separate admission conditions. The function only evaluates the ordering rule under a deliberately small contract.

worked-promotion-cases.py
from promotion_decision import promotion_decision

active = {"sequence": 204, "digest": "sha256:illustrative-204"}
late_build = {"sequence": 203, "digest": "sha256:illustrative-203"}
same_release = dict(active)
rollback_intent = {"sequence": 205, "digest": "sha256:illustrative-203"}

assert promotion_decision(late_build, active) == "reject: stale sequence"
assert promotion_decision(same_release, active) == "repeat: already active"
assert promotion_decision(
    rollback_intent, active
) == "advance: conditional promotion required"

Scenario: These strings are illustrative identifiers, not real content hashes. In the case, 203 becomes a recorded stale request after 204 is active. A request to restore 203’s artifact receives a new approved sequence 205, so the system can distinguish an intentional rollback from a late build. The decision still requires whatever compatibility and rollback checks the application needs; a larger sequence does not make an old schema compatible.

Make the target-state check and promotion one guarded operation. A controller cannot read the current release, release its lock and later promote the candidate without checking whether another writer changed the baseline. Use the deployment platform’s actual conditional-operation or shared-lock contract, and keep ownership until the result is reconciled. If no such contract exists, this function is insufficient to make parallel writers safe.

Record the expected active identity, proposed identity, approval reference, provider operation identifier and observed final identity. A timeout after submission means the outcome needs reconciliation. A retry should resume or inspect the recorded operation before creating another one. GitHub’s cancellation mechanism sends messages and signals to runners; it supplies no provider-specific receipt that a remote promotion rolled back. [3]

How do you verify the queue and the promotion guard?#

Test them as different mechanisms. In a safe environment, hold one participating deployment at the target gate and send additional approved requests into the same group. Observe which are retained, which are replaced under single and how capacity failures are surfaced. Do not infer the results from a green YAML check. Then arrange builds so a later approved release reaches the gate first.

  • Ordering inversion: promote the newer release first, then deliver the older one. Expect a stale disposition and the target still serving the newer release.
  • Repeat: resubmit the same active identity. Expect an already-active decision, without another remote promotion.
  • Identity collision: reuse the active sequence with a different digest. Expect rejection and an investigation of the release authority.
  • Unresolved operation: interrupt after the provider accepts a request. Expect a hold and reconciliation before another write.
  • Competing writer: change the target after the initial read. Expect the conditional promotion to fail or reacquire fresh authority.
  • Intentional rollback: approve a new sequence pointing at the older artifact, run compatibility checks and verify the actual resulting target.

The saved Python fixtures exercise the pure decision function only. They do not prove GitHub queue behavior, atomic promotion, artifact verification or provider idempotency. Confirm which immutable release the target serves, and record what happened to every retained request.

Environment approval and credential access remain separate from queue policy. GitHub Environments can require conditions before a job proceeds; a concurrency key is not an approval rule. Keep the Actions hardening controls and AWS OIDC trust checks connected to the deployment identity while testing ordering. [4]

Questions about Actions deployment queues#

Does cancel-in-progress false preserve every waiting deploy?
No. The default single pending slot can still be replaced by a newer same-group arrival. Current github.com documentation offers queue: max for multiple pending jobs or runs, subject to its limit. Decide whether distinct requests may be superseded before choosing the policy. [1][2]
Does queue max guarantee commit order?
No. GitHub describes FIFO by the time each job or run begins waiting on the concurrency group, not workflow dispatch time. Parallel builds can arrive in a different order. Use an explicit release contract and target-state check when older candidates must not replace newer ones. [1]
Can a group coordinate deployments from another repository?
The documented concurrency scope is the repository. A shared target with writers in other repositories or external tools needs a coordination mechanism all those writers honor. Merely reusing the same textual group name in different repositories does not establish that mechanism. [1]
Does a canceled job undo its remote deployment?
The cancellation reference describes runner cancellation, not a provider rollback contract. Inspect the recorded provider operation and target state before retrying. A canceled job may already have submitted work, so the remote outcome cannot be established from the workflow label alone. [3]
How can an intentional rollback pass a monotonic guard?
In this proposed contract, approve a new release intent with a new sequence that references the older artifact. Verify application and schema compatibility before promotion. This preserves the distinction between a requested rollback and a stale job that happened to finish late.

Technologies

  • GitHub Actions
  • Concurrency groups
  • Release promotion
  • Python 3

Sources

  1. [1]
    Control workflow concurrency, GitHub Docs

    Observed 20 October 2026. The default pending slot can be replaced. Queue wait order differs from workflow dispatch order. Max queue and running cancellation cannot be combined.

  2. [2]
    Concurrency groups now allow larger queues, GitHub Changelog

    Observed 20 October 2026. Multi-pending queues are a released capability, not the legacy single pending limit.

  3. [3]
    Workflow cancellation reference, GitHub Docs

    Observed 20 October 2026. Cancellation involves messages to runners; a run label is not remote rollback evidence.

  4. [4]
    Deployments and environments, GitHub Docs

    Observed 20 October 2026. Environment protection is a separate gate.

Written by

Dopstack Engineering

Engineering team

The engineering team at Dopstack. Illustrative scenarios are composites written to explain a pattern, not accounts of client work.

All articles Website

Get new articles by email

Engineering notes on AI, cloud and SaaS. Occasional, never sold or shared.

Send me new Dopstack Insights articles by email. I can unsubscribe at any time. We’ll email you once to confirm. Privacy

Continue reading