4.4 Concurrency Controls & Status Check Functions
Key Takeaways
- Concurrency groups (concurrency:) enforce execution mutual exclusion, ensuring only a single workflow run or job executes within a specified group key at any given time.
- The cancel-in-progress: true setting automatically cancels any currently executing run in the same concurrency group when a newer commit or run is queued.
- Status check functions (always(), success(), failure(), cancelled()) override the default implicit if: success() condition on jobs and steps.
- Cleanup and resource teardown steps must use if: always() to ensure that ephemeral cloud environments, test databases, or containers are destroyed even after test failures or cancellations.
- Setting strict job-level timeouts via timeout-minutes: is an essential operational best practice to prevent hung processes from consuming the default 360-minute execution window and exhausting runner budgets.
Concurrency Controls & Status Check Functions
In modern automated software delivery, managing simultaneous pipeline runs and controlling conditional execution based on runtime status are vital for stability, resource optimization, and cost governance. Without concurrency controls, rapid Git pushes can trigger redundant, wasteful builds that race against each other or deploy out-of-order changes to cloud infrastructure. Similarly, without status check functions, pipelines cannot reliably clean up ephemeral test resources when earlier steps fail.
1. Concurrency Controls (concurrency:)
The concurrency keyword controls the simultaneous execution of workflows or jobs that share the same concurrency group name. It can be declared at either the workflow level (affecting the entire workflow run) or at the job level (affecting only that specific job).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Key Parameters:
group(required): A string identifying the concurrency group. Expressions can dynamically construct group names using contexts likegithub.workflow,github.ref,github.head_ref, or job parameters.cancel-in-progress(optional, defaultfalse): When set totrue, GitHub Actions automatically cancels any currently executing job or workflow in that group whenever a new run is queued for the same group.
2. Common Enterprise Concurrency Patterns
Pattern A: Cancel Redundant Pull Request Builds
When developers rapidly push multiple commits to an open pull request, running CI tests on older, superseded commits wastes compute capacity. Use github.head_ref (available on pull_request events) falling back to github.ref:
name: Pull Request CI
on:
pull_request:
branches: [main]
concurrency:
group: pr-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
Pattern B: Serialized Production Deployments (No Cancellation)
For production deployments or database migrations, cancelling an in-progress deployment halfway through can leave cloud infrastructure in an inconsistent, corrupted state. Configure cancel-in-progress: false to ensure incoming releases wait in queue and execute sequentially:
name: Deploy Production
on:
push:
branches: [main]
concurrency:
group: production-environment-lock
cancel-in-progress: false # Default behavior: Queues sequentially
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Terraform Apply
run: terraform apply -auto-approve
[!IMPORTANT] Concurrency Queue Depth Limit: GitHub Actions maintains a maximum queue depth of 1 active run and 1 pending run per concurrency group. If multiple new runs are queued while one is currently running, GitHub retains the latest pending run and automatically cancels any intermediate pending runs.
3. Status Check Functions in Conditional Expressions
By default, every step and job in GitHub Actions has an implicit condition: if: success(). If any preceding step fails or if the workflow is cancelled, subsequent steps are skipped automatically. To override this default behavior, GitHub Actions provides four built-in status check functions:
| Status Function | Evaluates to true When: | Primary CI/CD Use Case |
|---|---|---|
success() | All previous steps/jobs have succeeded (no failures, no cancellations). | Standard sequential tasks (default behavior). |
always() | Always returns true, even if previous steps failed or the run was cancelled. | Teardown tasks, resource cleanup, log uploading. |
failure() | At least one previous step/job in the execution sequence has failed. | Incident alerts, Slack failure notifications, diagnostics. |
cancelled() | The workflow run was explicitly cancelled by a user or concurrency group. | Abort handlers, rolling back partial locks. |
Status Check Truth Table Across Step Execution States
| Previous Step State | if: success() | if: failure() | if: always() | if: cancelled() |
|---|---|---|---|---|
| All Preceding Steps Succeeded | true (Runs) | false (Skipped) | true (Runs) | false (Skipped) |
| A Preceding Step Failed | false (Skipped) | true (Runs) | true (Runs) | false (Skipped) |
| Workflow Cancelled by User | false (Skipped) | false (Skipped) | true (Runs) | true (Runs) |
| Preceding Step Skipped | true (Runs) | false (Skipped) | true (Runs) | false (Skipped) |
4. Teardown, Cleanup & Notification Patterns
In robust CI/CD engineering, provisioning temporary cloud testbeds, databases, or preview environments requires guaranteed teardown logic.
name: Integration Test with Cloud Teardown
on: [push]
jobs:
integration-suite:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Provision Ephemeral Test Database
id: provision
run: ./scripts/cloud-db-create.sh --id "test-${{ github.run_id }}"
- name: Run End-to-End Test Suite
run: npm run test:e2e
- name: Send Failure Alert to Incident Channel
if: failure() # Executes ONLY if provisioning or tests failed
run: |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"CI Failure on branch ${{ github.ref }}"}' \
${{ secrets.ALERT_WEBHOOK }}
- name: Teardown Ephemeral Test Database
if: always() # Executes ALWAYS, guaranteeing no orphaned cloud resources
run: ./scripts/cloud-db-destroy.sh --id "test-${{ github.run_id }}"
[!CAUTION] Evaluating Steps After
if: always(): If a cleanup step usingif: always()fails, subsequent steps without explicit conditions will be skipped because the job status transitions tofailed. Ensure all critical cleanup tasks either specifyif: always()or usecontinue-on-error: true.
5. Job Execution Timeouts (timeout-minutes:)
Every job in GitHub Actions should declare a reasonable timeout using timeout-minutes.
jobs:
build-and-test:
runs-on: ubuntu-latest
timeout-minutes: 15 # Terminates job if execution exceeds 15 minutes
steps:
- uses: actions/checkout@v4
- run: make test
Key Rules & Operational Best Practices:
- Default Timeout: If
timeout-minutesis not specified, GitHub Actions enforces a default timeout of 360 minutes (6 hours) per job. - Billing Protection: A hanging network call, unresponsive container, or deadlocked test process left unconstrained can consume up to 6 hours of runner capacity, exhausting organization concurrency limits and incurring massive billing charges.
- Step vs. Job Timeouts:
timeout-minutescan also be declared on individualsteps. Settingtimeout-minutes: 5on a deployment step ensures long-running network operations fail fast before consuming the entire job allocation.
A team wants to configure a workflow so that whenever a developer pushes a new commit to an active pull request, any currently running test workflow on that same pull request is immediately terminated to conserve runner minutes and run tests on the newest commit. Which configuration achieves this goal?
A workflow job provisions an expensive cloud test cluster in Step 1 and executes integration tests in Step 2. Which conditional expression must be specified on Step 3 (Cluster Teardown) to guarantee the cluster is destroyed even if Step 2 fails or a developer manually cancels the workflow run?
What is the default execution timeout for a GitHub Actions job if the timeout-minutes: property is not explicitly declared in the workflow file?