Skip to content
Français
CI/CD & Automatisation medium

Optimising GitHub Actions workflows

20 min de lecture

Read this page in French

A slow workflow is time lost for the whole team. Between waiting for test results and deployments that drag on, a badly optimised CI can cost hours every week. This module shows you how to cut the run time of your GitHub Actions workflows drastically, without sacrificing anything on the security side. It targets beginners and intermediate readers who already have a pipeline running and want to make it fast: cache, artifacts, parallelisation and debugging techniques.

What you will learn

  • Enable the cache of the dependencies for near-instant installs
  • Share a build between jobs with artifacts
  • Tell cache and artifacts apart so you stop confusing them
  • Parallelise the independent jobs and cancel the obsolete runs
  • Diagnose a workflow that is slow or that does not trigger

Where to start?

Optimisation follows a logical order: you start with the quickest win, the cache, then move up to the structural techniques.

  1. Enable the dependency cache

    This is the most immediate gain. One line is enough to go from 45 seconds to 3 seconds on the dependency install.

  2. Share the results between jobs with artifacts

    A build job creates dist/, the test and deploy jobs reuse it. You build only once for the whole pipeline.

  3. Understand the difference between cache and artifacts

    The cache reuses files between runs. Artifacts share files between the jobs of a single run. Confusing the two guarantees problems.

  4. Learn to diagnose problems

    A workflow that does not trigger, mysterious failures, unexplained slowness: debugging techniques will save you hours.

The guides of this module

Every guide of the module covers one isolated optimisation technique. Here is how they fit together.

Cache: speeding up the installs

The cache reuses the dependencies between runs. You push three commits in a row? The node_modules are downloaded only once.

Cache: the basics

Understanding keys, restore-keys, cache strategies and security. The complete guide to mastering the GitHub Actions cache.

Read the guide

Artifacts: sharing between jobs

Artifacts carry files between the jobs of a single run. A build job creates dist/, the following jobs download it. The guide covers upload, download, optimisations and the patterns to share builds, reports and binaries between jobs.

Read the guide

Debugging a workflow

Beyond cache and artifacts, debugging completes the toolkit: understanding why a workflow does not start, fails with no clear message or drags on. Concurrency, the other lever, is covered in the quick wins below.

Debugging workflows

A workflow that does not start? A failure with no clear message? Detective techniques to understand what happens on remote runners.

Read the guide

Quick wins

Five optimisations cover the vast majority of the gains. None of them takes more than a few lines of YAML.

1. Enable the built-in cache

The setup-* actions embed the cache in one line. No need to handle actions/cache by hand for the common cases:

# Node.js
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
cache: 'npm'
# Python
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
cache: 'pip'
# Java
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'

Typical gain: dependency install from 45 s down to 3 s.

2. Share the build with artifacts

Instead of rebuilding in every job, build once and share the result:

jobs:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Build the application
run: npm run build
- name: Publish the build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist
path: dist/
retention-days: 1
test:
needs: build
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Fetch the build
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: dist
- name: Run the tests
run: npm test

Typical gain: workflow from 10 min down to 6 min.

3. Cancel the obsolete runs

When you push several commits in a row, every push restarts the workflow. The concurrency block cancels the previous run:

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

Gain: saved build minutes, especially on active branches.

4. Parallelise the independent jobs

Lint, tests and build do not depend on each other: they can run in parallel. Only the deployment waits for everything with needs:.

jobs:
lint:
runs-on: ubuntu-24.04
steps:
- run: npm run lint
test:
runs-on: ubuntu-24.04
steps:
- run: npm test
build:
runs-on: ubuntu-24.04
steps:
- run: npm run build
deploy:
needs: [lint, test, build] # Waits for everything to finish
runs-on: ubuntu-24.04
steps:
- run: ./deploy.sh

Typical gain: 3 sequential jobs (15 min) down to 3 parallel jobs (5 min).

5. Filter the triggers

Rerunning the whole CI for a README change is a waste. paths-ignore filters those useless triggers:

on:
push:
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/**'
branches:
- main
- develop

Gain: fewer useless runs means less waiting and fewer minutes consumed.

The metrics to watch

Optimising without measuring means moving blindfolded. These five metrics tell you where the problem sits and whether your changes are paying off.

MetricWhere to find itTarget
Workflow durationActions tab< 10 min for a standard workflow
Cache hit rateJob logs> 80 % (effective cache)
Minutes consumedSettings, Actions, Usage< 50 % of the monthly quota
Install timesetup-* logs< 5 s with cache
Failure rateInsights, Actions< 5 % (stable quality)

Reasonable targets per job

  • Lint: < 1 min
  • Unit tests: < 5 min
  • Build: < 5 min
  • E2E tests: < 15 min
  • Deployment: < 3 min

If you go beyond those durations, there are probably optimisations to make.

Optimisation checklist

Go through this list before considering a workflow production ready: every ticked item corresponds to a measurable gain.

  • Cache enabled for the dependencies (cache: 'npm', cache: 'pip', etc.)
  • Builds shared through artifacts (no useless rebuild)
  • Independent jobs in parallel (lint, test, build)
  • concurrency configured to cancel the obsolete runs
  • Triggers filtered (paths-ignore for MD, docs and so on)
  • Minimal permissions (permissions: {} at workflow level, rights per job)
  • Total duration < 10 min for a standard workflow

An optimised workflow, end to end

Here is a complete workflow applying every good practice of the module, and staying compliant with the security rules: actions pinned by SHA, permissions: {} by default, persist-credentials: false.

name: Optimised CI/CD
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
# Cancels the obsolete runs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# No rights by default: every job asks for the minimum
permissions: {}
jobs:
# Parallel jobs to save time
lint:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Fetch the code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
cache: 'npm' # Cache enabled
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Fetch the code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
build:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Fetch the code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
# The build is shared through an artifact
- name: Publish the build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist
path: dist/
retention-days: 1
# Deployment once everything has passed
deploy:
needs: [lint, test, build]
runs-on: ubuntu-24.04
if: github.ref == 'refs/heads/main'
permissions:
contents: read
id-token: write # For OIDC
steps:
- name: Fetch the build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
- run: ./deploy.sh

What this workflow gains:

  • Cache: install from 45 s down to 3 s
  • Parallelisation: lint, test and build at the same time (5 min instead of 15 min)
  • Artifacts: no rebuild for the deployment
  • Concurrency: obsolete runs cancelled automatically
  • Filters: no run on documentation changes

Total duration: around 5 to 6 min, instead of 15 to 20 min without optimisation.

Key points

Four reflexes sum up the whole module. The rest is only tuning.

Cache = between runs

To reuse the dependencies between the runs of the workflow. An immediate gain on npm ci, pip install and the like.

Artifacts = between jobs

To share the results (builds, reports) between the jobs of a single run. Avoids useless rebuilds.

Parallelise

Lint, test and build can often run at the same time. The deployment waits with needs:.

Cancel the obsolete

concurrency: cancel-in-progress, to avoid burning runners on intermediate commits.

The 3 priority optimisations:

  1. Cache: add cache: 'npm' (or pip, maven and so on) to setup-*
  2. Artifacts: build once, share with upload-artifact and download-artifact
  3. Concurrency: cancel the obsolete runs automatically

Those three actions alone can divide the duration of your workflows by 2 or 3.

Next steps

  • Sharing between jobs (Artifacts): the upload and download mechanism that moves a binary or a report from one job to the next.
  • Speeding things up with the cache: the keys, the restore-keys and the security traps of the cache, the most profitable gain of the module.
  • Artifacts vs Cache: the arbitration between the two, once you have practised them separately.
  • Concurrency: the gain that costs nothing, cancelling the runs that have become useless before they consume minutes.

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn