A slow CI pipeline is usually treated as an annoyance to tolerate, and it is better understood as a change to how the team works. When feedback takes long enough that people start something else while waiting, they stop integrating frequently, batch up larger changes, and review with less context. The queue is the visible symptom; the behaviour change is the real cost, and it appears on no dashboard.
Before optimising anything, measure where the time actually goes, per stage, over a week. Teams consistently guess wrong. Suspicion falls on the test suite and the answer turns out to be dependency installation on a cold cache, container image pulls, or waiting for an available runner during the hours everyone works. Optimising a stage that is not the bottleneck produces a pipeline that is exactly as slow and a team now sceptical of the whole effort.
Caching is the first real lever and the one most often done badly. Dependencies, compiled artefacts and container layers should be cached with keys derived from the files that actually determine them - a lockfile hash, not a branch name. A cache keyed too loosely serves stale content and produces failures nobody can reproduce; keyed too tightly it never hits and quietly does nothing at all. The second failure is invisible unless somebody looks at hit rates.
Parallelism helps once caching is right, and the useful split is by time rather than by count. Dividing a suite into equal numbers of files leaves one shard running long after the others have finished, while splitting on recorded durations keeps them balanced. Running the fast checks - linting, type checking, unit tests - before slow integration stages returns most failures in a fraction of the time.
Not every check belongs on every commit. A full end-to-end suite against a real environment on every push to every branch is expensive and largely redundant. Run fast deterministic checks always, and run the heavy ones on merge to the main branch, on a schedule, or when a change touches relevant paths. This is a genuine trade and should be made deliberately: you are accepting a later signal in exchange for a much faster loop.
Flaky tests deserve separate treatment, because they are worse than slow ones. A suite that fails at random teaches the team to re-run rather than investigate, and once that habit exists a real failure gets re-run too. Quarantine them into a non-blocking stage with an owner and a date, and treat the quarantine list as debt to pay down rather than a place where things go to be forgotten.
The position worth holding is that pipeline speed is a product decision rather than housekeeping. If nobody owns it, it degrades steadily, because every individual addition is reasonable and the aggregate is not. A recurring, funded slot to keep the feedback loop short costs less than the slow erosion of how often people are willing to integrate.