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.
-
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.
-
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. -
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.
-
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.
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.
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.
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 testTypical 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: trueGain: 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.shTypical 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 - developGain: 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.
| Metric | Where to find it | Target |
|---|---|---|
| Workflow duration | Actions tab | < 10 min for a standard workflow |
| Cache hit rate | Job logs | > 80 % (effective cache) |
| Minutes consumed | Settings, Actions, Usage | < 50 % of the monthly quota |
| Install time | setup-* logs | < 5 s with cache |
| Failure rate | Insights, 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)
-
concurrencyconfigured to cancel the obsolete runs - Triggers filtered (
paths-ignorefor 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 runsconcurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true
# No rights by default: every job asks for the minimumpermissions: {}
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.shWhat 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:
- Cache: add
cache: 'npm'(orpip,mavenand so on) tosetup-* - Artifacts: build once, share with
upload-artifactanddownload-artifact - 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-keysand 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.