How to Cut GitHub Actions Cold Start and Setup Time
On a short pipeline, more time frequently goes to getting ready to work than to the work. Measure the gap between queued and the first real step before optimising anything downstream.
Every job pays a fixed cost before your build starts: waiting for a runner, booting it, checking out, installing a toolchain, and restoring dependencies. On a five-minute job that overhead is routinely half the wall clock, and it is paid again by every job in a matrix.
It is also the cheapest thing to fix, because unlike your build it is almost entirely configuration.
Measure the overhead
# queue time and total duration for recent runs
gh run list --limit 20 --json databaseId,createdAt,startedAt,updatedAt \
--jq '.[] | "queued: \((( .startedAt|fromdate) - (.createdAt|fromdate)))s total: \((( .updatedAt|fromdate) - (.startedAt|fromdate)))s"'
# then inside a job, time the setup itself
- run: echo "setup finished at $(date +%s)"Cut checkout
# default is already shallow, but make it explicit and skip submodules
- uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: false
# only deepen where you actually need history
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required for diffs, tags, git describeCut toolchain and dependency setup
- Use the
cache:option on setup actions rather than a separateactions/cachestep. It is keyed correctly for that ecosystem by default. - Key caches to the lockfile hash so an unchanged lockfile is an exact hit.
- Install browsers, databases, and system packages only in the jobs that use them.
- Prefer a prebuilt container image over installing packages at job start when the install is more than a few seconds.
- Run the cheapest gating checks first so an expensive job never starts on code that cannot pass.
Stop paying setup N times in a matrix
A matrix repeats the full fixed cost per entry. Before widening one, check that the overhead is not larger than the work being parallelised.
# 10 shards x 90s setup = 15 minutes of setup for one test suite.
# Fewer, longer shards are often faster in wall clock AND cheaper.
strategy:
matrix:
shard: [1, 2, 3, 4] # not [1..10] unless the tests justify itMeasure before you optimise
Pipeline optimisation usually targets the step people assume is slow. Get the real per-step timings first, because the answer is frequently dependency install or a cold cache rather than the build itself.
# per-job timings for the last 20 runs
gh run list --limit 20 --json databaseId,conclusion,createdAt,updatedAt \
--jq '.[] | "\(.conclusion)\t\(.createdAt)\t\(.updatedAt)"'
# per-step timing inside one run
gh run view <run-id> --log | grep -E "^\S+\s+.*Run |##\[group\]" | head -40Frequently asked questions
Why do my GitHub Actions jobs take so long to start?
How do I reduce GitHub Actions setup time?
cache: option on setup actions keyed to your lockfile, keep fetch-depth: 1 unless a job genuinely needs history, install browsers and services only where used, and avoid widening a matrix past the point where setup dominates.