Lesson 21 of 30
A pipeline that takes 25 minutes doesn't just cost more compute — it changes how people push code. Speed is a behavior lever, not a vanity metric.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Two costs hide inside every pipeline: the compute bill, and the time tax on every developer waiting for green. Most teams notice the bill first and optimize that. The bigger cost is usually the time tax — a slow pipeline changes behavior. If CI takes 25 minutes, people stop pushing small commits. They batch five changes into one push to "save a run." They stop watching the pipeline and start context-switching, which means a red build gets noticed an hour later instead of ninety seconds later. The whole discipline of fast, frequent integration quietly erodes, and nobody wrote a line of code that caused it — the pipeline's shape caused it.
So speed optimization isn't a nice-to-have pass at the end. It's what keeps the rest of your CI/CD practice actually being used the way it was designed to be used.
The single highest-leverage speed fix is running independent jobs concurrently instead of stacking them in one linear job. Compare a naive serial pipeline to one split by dependency:
# Before: everything in one job, ~14 min total, strictly serial
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pnpm install --frozen-lockfile # 2 min
- run: pnpm exec tsc --noEmit # 2 min
- run: pnpm exec eslint . # 1 min
- run: pnpm test # 4 min
- run: pnpm build # 5 min
# After: independent checks fan out, wall-clock ~= slowest job, not the sum
jobs:
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- uses: actions/cache/save@v4 # hand the node_modules cache to the fan-out
with:
path: node_modules
key: deps-${{ github.sha }}
typecheck:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache/restore@v4
with: { path: node_modules, key: deps-${{ github.sha }} }
- run: pnpm exec tsc --noEmit
lint:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache/restore@v4
with: { path: node_modules, key: deps-${{ github.sha }} }
- run: pnpm exec eslint .
test:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache/restore@v4
with: { path: node_modules, key: deps-${{ github.sha }} }
- run: pnpm test
typecheck, lint, and test all depend on install finishing, but not on each other — GitHub Actions runs them at the same time once install completes. The pipeline's wall-clock time drops from "sum of every step" to roughly "install + the slowest of the three parallel jobs." You pay for more runner-minutes in total (three runners instead of one), but the thing developers actually feel — how long they wait — shrinks dramatically.
Self-hosted runners are the other major lever, in the opposite direction: they trade convenience for cost. GitHub-hosted runners are metered per minute and spin up cold every time; a self-hosted runner (your own VM, or a pool via something like actions-runner-controller on Kubernetes) is a machine you keep warm, with dependencies pre-installed and caches persisted on disk. For a team burning thousands of CI minutes a month, that's a real bill reduction — but you take on patching, scaling, and security isolation between jobs yourself. It's a lever worth pulling once minutes-cost is a real line item, not a day-one optimization.
pnpm install on every run is often the single biggest chunk of pipeline time — before reaching for parallelization or self-hosted runners, make sure dependency caching is actually working (check the cache-hit log line, don't assume the key is right).Speed optimization is really dependency caching and job-graph design applied with a stopwatch — the same needs: job-dependency mechanics used to define a pipeline's stages, now pointed at wall-clock time instead of correctness.